├── .github └── workflows │ └── release.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE.txt ├── README.adoc ├── docs ├── img.png ├── schema-graph-flat.png ├── schema-graph.png └── using-kernel-tx.adoc ├── etc ├── checkstyle │ ├── config.xml │ └── suppressions.xml ├── jreleaser │ └── changelog.md.tpl └── license.tpl ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── org │ └── neo4j │ └── graph_schema │ └── introspector │ ├── GraphSchema.java │ ├── GraphSchemaGraphyResultWrapper.java │ ├── GraphSchemaJSONResultWrapper.java │ ├── GraphSchemaModule.java │ └── Introspect.java └── test ├── java └── org │ └── neo4j │ └── graph_schema │ └── introspector │ ├── GraphSchemaTest.java │ ├── IntrospectTest.java │ └── SamplingTest.java └── resources └── movie.cypher /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | create: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: 'Set up JDK' 14 | uses: actions/setup-java@v3 15 | with: 16 | distribution: zulu 17 | java-version: 17 18 | 19 | - name: 'Prepare git' 20 | run: git config --global core.autocrlf false 21 | 22 | - name: 'Prepare branch name' 23 | if: (github.event_name == 'create' && github.event.ref_type == 'tag') 24 | run: > 25 | echo "refName=${GITHUB_REF##*/}" >> $GITHUB_ENV 26 | 27 | - name: 'Checkout relevant branch' 28 | uses: actions/checkout@v3 29 | with: 30 | ref: ${{ env.refName }} 31 | 32 | - name: 'Create jar' 33 | run: > 34 | ./mvnw --no-transfer-progress -Dfast clean package 35 | 36 | - name: 'Create release' 37 | env: 38 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | run: ./mvnw --no-transfer-progress jreleaser:full-release 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### VS Code ### 31 | .vscode/ 32 | 33 | .DS_Store 34 | dependency-reduced-pom.xml 35 | \.*.swp 36 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j/graph-schema-introspector/8fbf26edfc5a5bbd5e55d73fc6dd139608dcf430/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip 18 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Neo4j Schema Introspector 2 | Michael Simons 3 | :doctype: article 4 | :lang: en 5 | :listing-caption: Listing 6 | :source-highlighter: coderay 7 | :icons: font 8 | // tag::properties[] 9 | :groupId: org.neo4j 10 | :artifactIdCore: neo4j-http 11 | :branch: main 12 | // end::properties[] 13 | 14 | [abstract] 15 | -- 16 | This is a Proof of concept (PoC) for a Neo4j schema introspector that produces output in JSON format validating against https://github.com/neo4j/graph-schema-json-js-utils[graph-schema-json-js-utils]. It is packaged as a Neo4j user-defined procedure under the name `experimental.introspect.asJson({})`. 17 | -- 18 | 19 | == Compiling 20 | 21 | You need Java 17 for this project to compile and package. It is build against Neo4j 5 and thus requires Neo4j 5 to run. 22 | 23 | Assuming Neo4j is installed in a `NEO4J_HOME`, please execute the following steps 24 | 25 | .Stop your Neo4j instance (optional) 26 | [source,bash] 27 | ---- 28 | $NEO4J_HOME/bin/neo4j stop 29 | ---- 30 | 31 | .Package this project 32 | [source,bash] 33 | ---- 34 | ./mvnw -Dfast clean package 35 | ---- 36 | 37 | .Copy the resulting artifact into your Neo4j installation and start the latter 38 | [source,bash] 39 | ---- 40 | cp -v target/graph-schema-introspector-*.jar $NEO4J_HOME/plugins 41 | $NEO4J_HOME/bin/neo4j start 42 | ---- 43 | 44 | After a bit, the instance will have started. For the sake of a functional readme, we assume a database password `verysecret`. 45 | 46 | == Running 47 | 48 | === Retrieve the schema as JSON document 49 | 50 | You call the procedure like this: 51 | 52 | [source,cypher] 53 | ---- 54 | CALL experimental.introspect.asJson({prettyPrint: true}) 55 | ---- 56 | 57 | This will return a single column named `value` containing the schema of the database as valid JSON for further processing. 58 | 59 | The following command will use Cypher-Shell to pipe the JSON (with some post-processing) through `jq` and eventually to standard out: 60 | 61 | [source,bash] 62 | ---- 63 | $NEO4J_HOME/bin/cypher-shell -uneo4j -pverysecret --format plain --non-interactive 'CALL experimental.introspect.asJson({}) YIELD value RETURN value AS _json_' | sed -e 's/\\"/"/g' -e 's/^"//g' -e 's/"\$//g' -e 's/_json_//g' -e 's/"{/{/' -e 's/}"/}/' | jq 64 | ---- 65 | 66 | NOTE: In the script above, https://stedolan.github.io/jq/[`jq`] is used to format the JSON. While the UDF can pretty print, Cypher-Shell always quotes quotes (turning `"` into `\"`), thus making the output useless. Therefor it is processed first with `sed` and then piped through `jq`. 67 | 68 | === Retrieve the schema in a graphy format 69 | 70 | A visualization of that schema can be generated with the following statement: 71 | 72 | [source,cypher] 73 | ---- 74 | CALL experimental.introspect.asGraph({}) 75 | ---- 76 | 77 | It will visualize the schema document, not the graph itself. The result for the Movie example graph will look like this (you may have to configure the corresponding properties to be used as display names): 78 | 79 | image::docs/schema-graph.png[] 80 | 81 | A graphy result that reassembles the current `db.schema.visualization` can be achieved with 82 | 83 | [source,cypher] 84 | ---- 85 | CALL experimental.introspect.asGraph({flat:true}) 86 | ---- 87 | 88 | The same schema as above will look like this: 89 | 90 | image::docs/schema-graph-flat.png[] 91 | 92 | In both cases you'll find a `properties` field on the properties of node- and relationship object types containing the property information in the same format as the JSON field. In the latter visualization nodes that have multiple labels will only spot the first one as a `name` property and the full list will be available as standard labels or in the `$id` field in case you did use constant ids. 93 | 94 | == Options 95 | 96 | The following options can be passed as arguments: 97 | 98 | |=== 99 | |Name |Type |Meaning |Default 100 | 101 | |`prettyPrint` 102 | |Boolean 103 | |Pretty prints the generated JSON 104 | |`false` 105 | 106 | |`useConstantIds` 107 | |Boolean 108 | |Uses constant ids for the tokens, derived from their names. Setting this to false generates unique ids 109 | |`true` 110 | 111 | |`quoteTokens` 112 | |Boolean 113 | |Tokens that would need quotation and sanitization when used in Cypher statements will be treated as such by default 114 | |`true` 115 | 116 | |`sampleOnly` 117 | |Boolean 118 | |By default, only 100 distinct relationships between two nodes are sampled to determine the concrete relationships (read: not only the type, but with start and end) owning a set of properties 119 | |`true` 120 | |=== 121 | -------------------------------------------------------------------------------- /docs/img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j/graph-schema-introspector/8fbf26edfc5a5bbd5e55d73fc6dd139608dcf430/docs/img.png -------------------------------------------------------------------------------- /docs/schema-graph-flat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j/graph-schema-introspector/8fbf26edfc5a5bbd5e55d73fc6dd139608dcf430/docs/schema-graph-flat.png -------------------------------------------------------------------------------- /docs/schema-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/neo4j/graph-schema-introspector/8fbf26edfc5a5bbd5e55d73fc6dd139608dcf430/docs/schema-graph.png -------------------------------------------------------------------------------- /docs/using-kernel-tx.adoc: -------------------------------------------------------------------------------- 1 | This is some example code how to retrieve node labels and types with Kernel-Transaction API, but I don't like to go there, 2 | procedure must be than unrestricted and all of that… 3 | 4 | [source,java] 5 | ---- 6 | if (callContext.isSystemDatabase()) { 7 | return List.of(); 8 | } 9 | 10 | var accessMode = transaction.securityContext().mode(); 11 | var schemaRead = transaction.schemaRead().snapshot(); 12 | Predicate labelFilter = token -> { 13 | var tokenId = token.id(); 14 | return hasAny(schemaRead.indexesGetForLabel(tokenId)) || 15 | hasAny(schemaRead.constraintsGetForLabel(tokenId)) || 16 | transaction.dataRead().countsForNode(tokenId) > 0; 17 | }; 18 | labelFilter = labelFilter.and(token -> accessMode.allowsTraverseNode(token.id())); 19 | 20 | return StreamSupport.stream(Spliterators.spliteratorUnknownSize(transaction.tokenRead().labelsGetAllTokens(), Spliterator.ORDERED), false) 21 | .filter(labelFilter) 22 | .map(token -> new NodeLabel(TSID_FACTORY.create().format("nl:%s"), token.name())) 23 | .toList(); 24 | 25 | 26 | private static boolean hasAny(Iterator iter) { 27 | try { 28 | return iter.hasNext(); 29 | } finally { 30 | if (iter instanceof Resource) { 31 | ((Resource) iter).close(); 32 | } 33 | } 34 | } 35 | ---- -------------------------------------------------------------------------------- /etc/checkstyle/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 100 | 101 | 102 | 103 | 105 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /etc/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /etc/jreleaser/changelog.md.tpl: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | {{changelogChanges}} 4 | -------------------------------------------------------------------------------- /etc/license.tpl: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 "Neo4j," 2 | Neo4j Sweden AB [https://neo4j.com] 3 | 4 | This file is part of Neo4j. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | https://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Apache Maven Wrapper startup batch script, version 3.1.1 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | # e.g. to debug Maven itself, use 32 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | # ---------------------------------------------------------------------------- 35 | 36 | if [ -z "$MAVEN_SKIP_RC" ] ; then 37 | 38 | if [ -f /usr/local/etc/mavenrc ] ; then 39 | . /usr/local/etc/mavenrc 40 | fi 41 | 42 | if [ -f /etc/mavenrc ] ; then 43 | . /etc/mavenrc 44 | fi 45 | 46 | if [ -f "$HOME/.mavenrc" ] ; then 47 | . "$HOME/.mavenrc" 48 | fi 49 | 50 | fi 51 | 52 | # OS specific support. $var _must_ be set to either true or false. 53 | cygwin=false; 54 | darwin=false; 55 | mingw=false 56 | case "`uname`" in 57 | CYGWIN*) cygwin=true ;; 58 | MINGW*) mingw=true;; 59 | Darwin*) darwin=true 60 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 61 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 62 | if [ -z "$JAVA_HOME" ]; then 63 | if [ -x "/usr/libexec/java_home" ]; then 64 | JAVA_HOME="`/usr/libexec/java_home`"; export JAVA_HOME 65 | else 66 | JAVA_HOME="/Library/Java/Home"; export JAVA_HOME 67 | fi 68 | fi 69 | ;; 70 | esac 71 | 72 | if [ -z "$JAVA_HOME" ] ; then 73 | if [ -r /etc/gentoo-release ] ; then 74 | JAVA_HOME=`java-config --jre-home` 75 | fi 76 | fi 77 | 78 | # For Cygwin, ensure paths are in UNIX format before anything is touched 79 | if $cygwin ; then 80 | [ -n "$JAVA_HOME" ] && 81 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 82 | [ -n "$CLASSPATH" ] && 83 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 84 | fi 85 | 86 | # For Mingw, ensure paths are in UNIX format before anything is touched 87 | if $mingw ; then 88 | [ -n "$JAVA_HOME" ] && 89 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 90 | fi 91 | 92 | if [ -z "$JAVA_HOME" ]; then 93 | javaExecutable="`which javac`" 94 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 95 | # readlink(1) is not available as standard on Solaris 10. 96 | readLink=`which readlink` 97 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 98 | if $darwin ; then 99 | javaHome="`dirname \"$javaExecutable\"`" 100 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 101 | else 102 | javaExecutable="`readlink -f \"$javaExecutable\"`" 103 | fi 104 | javaHome="`dirname \"$javaExecutable\"`" 105 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 106 | JAVA_HOME="$javaHome" 107 | export JAVA_HOME 108 | fi 109 | fi 110 | fi 111 | 112 | if [ -z "$JAVACMD" ] ; then 113 | if [ -n "$JAVA_HOME" ] ; then 114 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 115 | # IBM's JDK on AIX uses strange locations for the executables 116 | JAVACMD="$JAVA_HOME/jre/sh/java" 117 | else 118 | JAVACMD="$JAVA_HOME/bin/java" 119 | fi 120 | else 121 | JAVACMD="`\\unset -f command; \\command -v java`" 122 | fi 123 | fi 124 | 125 | if [ ! -x "$JAVACMD" ] ; then 126 | echo "Error: JAVA_HOME is not defined correctly." >&2 127 | echo " We cannot execute $JAVACMD" >&2 128 | exit 1 129 | fi 130 | 131 | if [ -z "$JAVA_HOME" ] ; then 132 | echo "Warning: JAVA_HOME environment variable is not set." 133 | fi 134 | 135 | # traverses directory structure from process work directory to filesystem root 136 | # first directory with .mvn subdirectory is considered project base directory 137 | find_maven_basedir() { 138 | if [ -z "$1" ] 139 | then 140 | echo "Path not specified to find_maven_basedir" 141 | return 1 142 | fi 143 | 144 | basedir="$1" 145 | wdir="$1" 146 | while [ "$wdir" != '/' ] ; do 147 | if [ -d "$wdir"/.mvn ] ; then 148 | basedir=$wdir 149 | break 150 | fi 151 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 152 | if [ -d "${wdir}" ]; then 153 | wdir=`cd "$wdir/.."; pwd` 154 | fi 155 | # end of workaround 156 | done 157 | printf '%s' "$(cd "$basedir"; pwd)" 158 | } 159 | 160 | # concatenates all lines of a file 161 | concat_lines() { 162 | if [ -f "$1" ]; then 163 | echo "$(tr -s '\n' ' ' < "$1")" 164 | fi 165 | } 166 | 167 | BASE_DIR=$(find_maven_basedir "$(dirname $0)") 168 | if [ -z "$BASE_DIR" ]; then 169 | exit 1; 170 | fi 171 | 172 | MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR 173 | if [ "$MVNW_VERBOSE" = true ]; then 174 | echo $MAVEN_PROJECTBASEDIR 175 | fi 176 | 177 | ########################################################################################## 178 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 179 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 180 | ########################################################################################## 181 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 182 | if [ "$MVNW_VERBOSE" = true ]; then 183 | echo "Found .mvn/wrapper/maven-wrapper.jar" 184 | fi 185 | else 186 | if [ "$MVNW_VERBOSE" = true ]; then 187 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 188 | fi 189 | if [ -n "$MVNW_REPOURL" ]; then 190 | wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 191 | else 192 | wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 193 | fi 194 | while IFS="=" read key value; do 195 | case "$key" in (wrapperUrl) wrapperUrl="$value"; break ;; 196 | esac 197 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 198 | if [ "$MVNW_VERBOSE" = true ]; then 199 | echo "Downloading from: $wrapperUrl" 200 | fi 201 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 202 | if $cygwin; then 203 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 204 | fi 205 | 206 | if command -v wget > /dev/null; then 207 | QUIET="--quiet" 208 | if [ "$MVNW_VERBOSE" = true ]; then 209 | echo "Found wget ... using wget" 210 | QUIET="" 211 | fi 212 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 213 | wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" 214 | else 215 | wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" 216 | fi 217 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 218 | elif command -v curl > /dev/null; then 219 | QUIET="--silent" 220 | if [ "$MVNW_VERBOSE" = true ]; then 221 | echo "Found curl ... using curl" 222 | QUIET="" 223 | fi 224 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 225 | curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L 226 | else 227 | curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L 228 | fi 229 | [ $? -eq 0 ] || rm -f "$wrapperJarPath" 230 | else 231 | if [ "$MVNW_VERBOSE" = true ]; then 232 | echo "Falling back to using Java to download" 233 | fi 234 | javaSource="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 235 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" 236 | # For Cygwin, switch paths to Windows format before running javac 237 | if $cygwin; then 238 | javaSource=`cygpath --path --windows "$javaSource"` 239 | javaClass=`cygpath --path --windows "$javaClass"` 240 | fi 241 | if [ -e "$javaSource" ]; then 242 | if [ ! -e "$javaClass" ]; then 243 | if [ "$MVNW_VERBOSE" = true ]; then 244 | echo " - Compiling MavenWrapperDownloader.java ..." 245 | fi 246 | # Compiling the Java class 247 | ("$JAVA_HOME/bin/javac" "$javaSource") 248 | fi 249 | if [ -e "$javaClass" ]; then 250 | # Running the downloader 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo " - Running MavenWrapperDownloader.java ..." 253 | fi 254 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 255 | fi 256 | fi 257 | fi 258 | fi 259 | ########################################################################################## 260 | # End of extension 261 | ########################################################################################## 262 | 263 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 264 | 265 | # For Cygwin, switch paths to Windows format before running java 266 | if $cygwin; then 267 | [ -n "$JAVA_HOME" ] && 268 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 269 | [ -n "$CLASSPATH" ] && 270 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 271 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 272 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 273 | fi 274 | 275 | # Provide a "standardized" way to retrieve the CLI args that will 276 | # work with both Windows and non-Windows executions. 277 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 278 | export MAVEN_CMD_LINE_ARGS 279 | 280 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 281 | 282 | exec "$JAVACMD" \ 283 | $MAVEN_OPTS \ 284 | $MAVEN_DEBUG_OPTS \ 285 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 286 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 287 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 288 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Apache Maven Wrapper startup batch script, version 3.1.1 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 28 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 29 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 30 | @REM e.g. to debug Maven itself, use 31 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 32 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 33 | @REM ---------------------------------------------------------------------------- 34 | 35 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 36 | @echo off 37 | @REM set title of command window 38 | title %0 39 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 40 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 41 | 42 | @REM set %HOME% to equivalent of $HOME 43 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 44 | 45 | @REM Execute a user defined script before this one 46 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 47 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 48 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* 49 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* 50 | :skipRcPre 51 | 52 | @setlocal 53 | 54 | set ERROR_CODE=0 55 | 56 | @REM To isolate internal variables from possible post scripts, we use another setlocal 57 | @setlocal 58 | 59 | @REM ==== START VALIDATION ==== 60 | if not "%JAVA_HOME%" == "" goto OkJHome 61 | 62 | echo. 63 | echo Error: JAVA_HOME not found in your environment. >&2 64 | echo Please set the JAVA_HOME variable in your environment to match the >&2 65 | echo location of your Java installation. >&2 66 | echo. 67 | goto error 68 | 69 | :OkJHome 70 | if exist "%JAVA_HOME%\bin\java.exe" goto init 71 | 72 | echo. 73 | echo Error: JAVA_HOME is set to an invalid directory. >&2 74 | echo JAVA_HOME = "%JAVA_HOME%" >&2 75 | echo Please set the JAVA_HOME variable in your environment to match the >&2 76 | echo location of your Java installation. >&2 77 | echo. 78 | goto error 79 | 80 | @REM ==== END VALIDATION ==== 81 | 82 | :init 83 | 84 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 85 | @REM Fallback to current working directory if not found. 86 | 87 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 88 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 89 | 90 | set EXEC_DIR=%CD% 91 | set WDIR=%EXEC_DIR% 92 | :findBaseDir 93 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 94 | cd .. 95 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 96 | set WDIR=%CD% 97 | goto findBaseDir 98 | 99 | :baseDirFound 100 | set MAVEN_PROJECTBASEDIR=%WDIR% 101 | cd "%EXEC_DIR%" 102 | goto endDetectBaseDir 103 | 104 | :baseDirNotFound 105 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 106 | cd "%EXEC_DIR%" 107 | 108 | :endDetectBaseDir 109 | 110 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 111 | 112 | @setlocal EnableExtensions EnableDelayedExpansion 113 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 114 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 115 | 116 | :endReadAdditionalConfig 117 | 118 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 121 | 122 | set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 123 | 124 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 125 | IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B 126 | ) 127 | 128 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 129 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 130 | if exist %WRAPPER_JAR% ( 131 | if "%MVNW_VERBOSE%" == "true" ( 132 | echo Found %WRAPPER_JAR% 133 | ) 134 | ) else ( 135 | if not "%MVNW_REPOURL%" == "" ( 136 | SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar" 137 | ) 138 | if "%MVNW_VERBOSE%" == "true" ( 139 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 140 | echo Downloading from: %WRAPPER_URL% 141 | ) 142 | 143 | powershell -Command "&{"^ 144 | "$webclient = new-object System.Net.WebClient;"^ 145 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 146 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 147 | "}"^ 148 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ 149 | "}" 150 | if "%MVNW_VERBOSE%" == "true" ( 151 | echo Finished downloading %WRAPPER_JAR% 152 | ) 153 | ) 154 | @REM End of extension 155 | 156 | @REM Provide a "standardized" way to retrieve the CLI args that will 157 | @REM work with both Windows and non-Windows executions. 158 | set MAVEN_CMD_LINE_ARGS=%* 159 | 160 | %MAVEN_JAVA_EXE% ^ 161 | %JVM_CONFIG_MAVEN_PROPS% ^ 162 | %MAVEN_OPTS% ^ 163 | %MAVEN_DEBUG_OPTS% ^ 164 | -classpath %WRAPPER_JAR% ^ 165 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ 166 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 167 | if ERRORLEVEL 1 goto error 168 | goto end 169 | 170 | :error 171 | set ERROR_CODE=1 172 | 173 | :end 174 | @endlocal & set ERROR_CODE=%ERROR_CODE% 175 | 176 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost 177 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 178 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" 179 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" 180 | :skipRcPost 181 | 182 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 183 | if "%MAVEN_BATCH_PAUSE%"=="on" pause 184 | 185 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% 186 | 187 | cmd /C exit /B %ERROR_CODE% 188 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 22 | 23 | 4.0.0 24 | 25 | org.neo4j.graph-schema 26 | graph-schema-introspector 27 | 1.0.0-SNAPSHOT 28 | 29 | jar 30 | Neo4j Schema Introspector 31 | UDF for estimating a Neo4j schema and exporting it into graph-schema-json. 32 | 33 | 2023 34 | 35 | Neo4j, Neo4j Sweden AB 36 | https://neo4j.com 37 | 38 | 39 | 40 | The Apache Software License, Version 2.0 41 | https://www.apache.org/licenses/LICENSE-2.0.txt 42 | repo 43 | 44 | 45 | 46 | 47 | 48 | msimons 49 | Michael Simons 50 | michael.simons at neo4j.com 51 | Neo Technology 52 | https://neo4j.com 53 | 54 | Project Lead 55 | 56 | +1 57 | 58 | 59 | 60 | 61 | ${project.build.directory} 62 | 3.22.0 63 | 10.6.0 64 | 2023.2.0 65 | 2.14.1 66 | 17 67 | 1.5.1 68 | 5.9.2 69 | 4.2.rc2 70 | 3.2.1 71 | 3.10.1 72 | 3.0.0 73 | 3.4.1 74 | 3.0.0 75 | ${java.version} 76 | 3.8.7 77 | 5.6.0 78 | 5.3.0 79 | UTF-8 80 | UTF-8 81 | 3.2.0 82 | 5.2.0 83 | 84 | 85 | 86 | 87 | 88 | com.fasterxml.jackson 89 | jackson-bom 90 | ${jackson.version} 91 | pom 92 | import 93 | 94 | 95 | 96 | 97 | 98 | 99 | com.fasterxml.jackson.core 100 | jackson-databind 101 | 102 | 103 | com.github.f4b6a3 104 | tsid-creator 105 | ${tsid-creator.version} 106 | 107 | 108 | org.neo4j 109 | neo4j-cypher-dsl-schema-name-support 110 | ${cypher-dsl.version} 111 | 112 | 113 | 114 | org.neo4j 115 | neo4j 116 | ${neo4j.version} 117 | provided 118 | 119 | 120 | 121 | org.assertj 122 | assertj-core 123 | ${assertj.version} 124 | test 125 | 126 | 127 | org.junit.jupiter 128 | junit-jupiter-engine 129 | ${junit-jupiter.version} 130 | test 131 | 132 | 133 | org.neo4j.driver 134 | neo4j-java-driver 135 | ${neo4j-java-driver.version} 136 | test 137 | 138 | 139 | org.neo4j.test 140 | neo4j-harness 141 | ${neo4j.version} 142 | test 143 | 144 | 145 | 146 | 147 | 148 | 149 | com.mycila 150 | license-maven-plugin 151 | ${license-maven-plugin.version} 152 | 153 |
etc/license.tpl
154 | true 155 | 156 | SCRIPT_STYLE 157 | 158 | 159 | 2023 160 | 161 | 162 | ** 163 | 164 | 165 | **/*.adoc 166 | **/*.cypher 167 | **/*.tpl 168 | LICENSE.txt 169 | 170 |
171 | 172 | 173 | validate 174 | 175 | check 176 | 177 | validate 178 | 179 | 180 |
181 | 182 | com.github.ekryd.sortpom 183 | sortpom-maven-plugin 184 | ${sortpom-maven-plugin.version} 185 | 186 | ${project.build.sourceEncoding} 187 | true 188 | -1 189 | true 190 | scope,groupId,artifactId 191 | false 192 | false 193 | 194 | 195 | 196 | 197 | sort 198 | 199 | verify 200 | 201 | 202 | 203 | 204 | org.apache.maven.plugins 205 | maven-enforcer-plugin 206 | ${maven-enforcer-plugin.version} 207 | 208 | 209 | enforce 210 | 211 | enforce 212 | 213 | validate 214 | 215 | 216 | 217 | ${java.version} 218 | 219 | 220 | ${maven.version} 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | org.apache.maven.plugins 229 | maven-checkstyle-plugin 230 | ${maven-checkstyle-plugin.version} 231 | 232 | etc/checkstyle/config.xml 233 | etc/checkstyle/suppressions.xml 234 | ${project.build.sourceEncoding} 235 | true 236 | true 237 | true 238 | 239 | 240 | 241 | com.puppycrawl.tools 242 | checkstyle 243 | ${checkstyle.version} 244 | 245 | 246 | 247 | 248 | validate 249 | 250 | check 251 | 252 | validate 253 | 254 | 255 | 256 | 257 | org.apache.maven.plugins 258 | maven-compiler-plugin 259 | ${maven-compiler-plugin.version} 260 | 261 | 262 | org.apache.maven.plugins 263 | maven-surefire-plugin 264 | ${maven-surefire-plugin.version} 265 | 266 | 267 | org.apache.maven.plugins 268 | maven-shade-plugin 269 | ${maven-shade-plugin.version} 270 | 271 | false 272 | 273 | 274 | *:* 275 | 276 | module-info.class 277 | META-INF/versions/9/module-info.class 278 | META-INF/MANIFEST.MF 279 | META-INF/NOTICE 280 | META-INF/LICENSE 281 | META-INF/*.SF 282 | META-INF/*.DSA 283 | META-INF/*.RSA 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | com.fasterxml 293 | org.neo4j.graph_schema.introspector.internal.jackson 294 | 295 | 296 | com.github.f4b6a3.tsid 297 | org.neo4j.graph_schema.introspector.internal.tsid 298 | 299 | 300 | org.neo4j.cypherdsl.support.schema_name 301 | org.neo4j.graph_schema.introspector.internal.schema_name 302 | 303 | 304 | 305 | 306 | 307 | 308 | shade 309 | 310 | package 311 | 312 | 313 | 314 | 315 | org.jreleaser 316 | jreleaser-maven-plugin 317 | ${jreleaser-maven-plugin.version} 318 | 319 | 320 | 321 | ${project.artifactId} 322 | 323 | 324 | 325 | true 326 | {{projectVersion}} 327 | {{projectVersion}} 328 | main 329 | 330 | ALWAYS 331 | etc/jreleaser/changelog.md.tpl 332 | - {{commitShortHash}} {{commitTitle}} 333 | conventional-commits 334 | 335 | 336 | 337 | 338 | 339 | SINGLE_JAR 340 | 341 | 342 | {{artifactsDir}}/{{projectName}}-{{projectVersion}}.jar 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 |
351 |
352 | 353 | 354 | 355 | fast 356 | 357 | 358 | fast 359 | 360 | 361 | 362 | true 363 | true 364 | true 365 | true 366 | true 367 | true 368 | true 369 | true 370 | true 371 | true 372 | true 373 | true 374 | true 375 | 376 | 377 | 378 |
379 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/graph_schema/introspector/GraphSchema.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 "Neo4j," 3 | * Neo4j Sweden AB [https://neo4j.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | package org.neo4j.graph_schema.introspector; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.HashMap; 24 | import java.util.LinkedHashMap; 25 | import java.util.List; 26 | import java.util.Locale; 27 | import java.util.Map; 28 | import java.util.Optional; 29 | import java.util.concurrent.ThreadLocalRandom; 30 | import java.util.function.BinaryOperator; 31 | import java.util.function.Function; 32 | import java.util.function.Predicate; 33 | import java.util.function.Supplier; 34 | import java.util.function.UnaryOperator; 35 | import java.util.regex.Pattern; 36 | import java.util.stream.Collectors; 37 | import java.util.stream.StreamSupport; 38 | 39 | import org.neo4j.cypherdsl.support.schema_name.SchemaNames; 40 | import org.neo4j.graph_schema.introspector.Introspect.Config; 41 | import org.neo4j.graphdb.Label; 42 | import org.neo4j.graphdb.RelationshipType; 43 | import org.neo4j.graphdb.Resource; 44 | import org.neo4j.graphdb.Result; 45 | import org.neo4j.graphdb.Transaction; 46 | 47 | import com.github.f4b6a3.tsid.TsidFactory; 48 | 49 | /** 50 | * The schema derived. 51 | */ 52 | final class GraphSchema { 53 | 54 | static GraphSchema build(Transaction transaction, Config config) throws Exception { 55 | return new Introspector(transaction, config).introspect(); 56 | } 57 | 58 | /** 59 | * Map from label (string value) to token. 60 | */ 61 | private final Map nodeLabels; 62 | /** 63 | * Map from type (string value) to token. 64 | */ 65 | private final Map relationshipTypes; 66 | /** 67 | * Map from generated ID to instance. 68 | */ 69 | private final Map nodeObjectTypes; 70 | /** 71 | * Map from generated ID to instance. 72 | */ 73 | private final Map relationshipObjectTypes; 74 | 75 | GraphSchema(Map nodeLabels, Map relationshipTypes, Map nodeObjectTypes, Map relationshipObjectTypes) { 76 | this.nodeLabels = nodeLabels; 77 | this.relationshipTypes = relationshipTypes; 78 | this.nodeObjectTypes = nodeObjectTypes; 79 | this.relationshipObjectTypes = relationshipObjectTypes; 80 | } 81 | 82 | public Map nodeLabels() { 83 | return nodeLabels; 84 | } 85 | 86 | public Map relationshipTypes() { 87 | return relationshipTypes; 88 | } 89 | 90 | public Map nodeObjectTypes() { 91 | return nodeObjectTypes; 92 | } 93 | 94 | public Map relationshipObjectTypes() { 95 | return relationshipObjectTypes; 96 | } 97 | 98 | record Type(String value, String itemType) { 99 | } 100 | 101 | record Property(String token, List types, boolean mandatory) { 102 | } 103 | 104 | record NodeObjectType(String id, List labels, List properties) { 105 | 106 | NodeObjectType(String id, List labels) { 107 | this(id, labels, new ArrayList<>()); // Mutable on purpose 108 | } 109 | } 110 | 111 | record Token(String id, String value) { 112 | } 113 | 114 | record Ref(String value) { 115 | } 116 | 117 | record RelationshipObjectType(String id, Ref type, Ref from, Ref to, List properties) { 118 | 119 | RelationshipObjectType(String id, Ref type, Ref from, Ref to) { 120 | this(id, type, from, to, new ArrayList<>()); // Mutable on purpose 121 | } 122 | } 123 | 124 | static class Introspector { 125 | 126 | /** 127 | * Number of relationships to sample, defaults to the same value as used in APOC and GraphQL introspection as of writing. 128 | */ 129 | static final Long DEFAULT_SAMPLE_SIZE = 100L; 130 | 131 | private static final Supplier ID_GENERATOR = new Supplier<>() { 132 | private final TsidFactory internalFactory = TsidFactory.builder() 133 | .withRandomFunction(length -> { 134 | final byte[] bytes = new byte[length]; 135 | ThreadLocalRandom.current().nextBytes(bytes); 136 | return bytes; 137 | }).build(); 138 | 139 | @Override 140 | public String get() { 141 | return internalFactory.create().format("%S"); 142 | } 143 | }; 144 | 145 | private static final Pattern ENCLOSING_TICK_MARKS = Pattern.compile("^`(.+)`$"); 146 | private static final Map TYPE_MAPPING = Map.of( 147 | "Long", "integer", 148 | "Double", "float" 149 | ); 150 | 151 | private final Transaction transaction; 152 | 153 | private final Config config; 154 | 155 | private Introspector(Transaction transaction, Config config) { 156 | this.transaction = transaction; 157 | this.config = config; 158 | } 159 | 160 | GraphSchema introspect() throws Exception { 161 | var nodeLabels = getNodeLabels(); 162 | var relationshipTypes = getRelationshipTypes(); 163 | 164 | var nodeObjectTypeIdGenerator = new CachingUnaryOperator<>(new NodeObjectIdGenerator(config.useConstantIds())); 165 | var relationshipObjectIdGenerator = new RelationshipObjectIdGenerator(config.useConstantIds()); 166 | 167 | var nodeObjectTypes = getNodeObjectTypes(nodeObjectTypeIdGenerator, nodeLabels); 168 | var relationshipObjectTypes = getRelationshipObjectTypes(nodeObjectTypeIdGenerator, relationshipObjectIdGenerator, relationshipTypes); 169 | 170 | return new GraphSchema(nodeLabels, relationshipTypes, nodeObjectTypes, relationshipObjectTypes); 171 | } 172 | 173 | private Map getNodeLabels() throws Exception { 174 | 175 | return getToken(transaction.getAllLabelsInUse(), Label::name, config.quoteTokens(), config.useConstantIds() ? "nl:%s"::formatted : ignored -> ID_GENERATOR.get()); 176 | } 177 | 178 | private Map getRelationshipTypes() throws Exception { 179 | 180 | return getToken(transaction.getAllRelationshipTypesInUse(), RelationshipType::name, config.quoteTokens(), config.useConstantIds() ? "rt:%s"::formatted : ignored -> ID_GENERATOR.get()); 181 | } 182 | 183 | private Map getToken(Iterable tokensInUse, Function nameExtractor, boolean quoteTokens, UnaryOperator idGenerator) throws Exception { 184 | 185 | Function valueMapper = Function.identity(); 186 | if (quoteTokens) { 187 | valueMapper = token -> new Token(token.id(), SchemaNames.sanitize(token.value()).orElse(token.value())); 188 | } 189 | try { 190 | return StreamSupport.stream(tokensInUse.spliterator(), false) 191 | .map(label -> { 192 | var tokenValue = nameExtractor.apply(label); 193 | return new Token(idGenerator.apply(tokenValue), tokenValue); 194 | }) 195 | .collect(Collectors.toMap(Token::value, valueMapper)); 196 | } finally { 197 | if (tokensInUse instanceof Resource resource) { 198 | resource.close(); 199 | } 200 | } 201 | } 202 | 203 | private static String getRelationshipPropertiesQuery(Config config) { 204 | // language=cypher 205 | var template = """ 206 | CALL db.schema.relTypeProperties() YIELD relType, propertyName, propertyTypes, mandatory 207 | WITH substring(relType, 2, size(relType)-3) AS relType, propertyName, propertyTypes, mandatory 208 | CALL { 209 | WITH relType, propertyName 210 | MATCH (n)-[r]->(m) WHERE type(r) = relType AND (r[propertyName] IS NOT NULL OR propertyName IS NULL) 211 | WITH n, r, m 212 | // LIMIT 213 | WITH DISTINCT labels(n) AS from, labels(m) AS to 214 | RETURN from, to 215 | } 216 | RETURN DISTINCT from, to, relType, propertyName, propertyTypes, mandatory 217 | ORDER BY relType ASC 218 | """; 219 | if (config.sampleOnly()) { 220 | return template.replace("// LIMIT\n", "LIMIT " + DEFAULT_SAMPLE_SIZE + "\n"); 221 | } 222 | return template; 223 | } 224 | 225 | /** 226 | * The main algorithm of retrieving node object types (or instances). It uses the existing procedure {@code db.schema.nodeTypeProperties} 227 | * for building a map from nodeType to property sets. 228 | * 229 | * @param idGenerator The id generator 230 | * @param labelIdToToken The map of existing token by id 231 | * @return A map with the node object instances 232 | * @throws Exception Any exception that might occur 233 | */ 234 | private Map getNodeObjectTypes(UnaryOperator idGenerator, Map labelIdToToken) throws Exception { 235 | 236 | if (labelIdToToken.isEmpty()) { 237 | return Map.of(); 238 | } 239 | 240 | // language=cypher 241 | var query = """ 242 | CALL db.schema.nodeTypeProperties() 243 | YIELD nodeType, nodeLabels, propertyName, propertyTypes, mandatory 244 | RETURN * 245 | ORDER BY nodeType ASC 246 | """; 247 | 248 | var nodeObjectTypes = new LinkedHashMap(); 249 | transaction.execute(query).accept((Result.ResultVisitor) resultRow -> { 250 | @SuppressWarnings("unchecked") 251 | var nodeLabels = ((List) resultRow.get("nodeLabels")).stream().sorted().toList(); 252 | 253 | var id = new Ref(idGenerator.apply(resultRow.getString("nodeType"))); 254 | var nodeObject = nodeObjectTypes.computeIfAbsent(id, key -> new GraphSchema.NodeObjectType(key.value, nodeLabels 255 | .stream().map(l -> new Ref(labelIdToToken.get(l).id)).toList())); 256 | extractProperty(resultRow) 257 | .ifPresent(nodeObject.properties()::add); 258 | 259 | return true; 260 | }); 261 | return nodeObjectTypes; 262 | } 263 | 264 | /** 265 | * The main algorithm of retrieving node object types (or instances). It uses the existing procedure {@literal db.schema.relTypeProperties} 266 | * for building a map from types to property sets. 267 | *

268 | * It does a full label scan. 269 | * 270 | * @param nodeObjectTypeIdGenerator The id generator f or node objects 271 | * @param idGenerator The id generator for relationships 272 | * @param relationshipIdToToken The map of existing token by id 273 | * @return A map with the relationship object instances 274 | * @throws Exception Any exception that might occur 275 | */ 276 | private Map getRelationshipObjectTypes( 277 | UnaryOperator nodeObjectTypeIdGenerator, 278 | BinaryOperator idGenerator, 279 | Map relationshipIdToToken 280 | ) throws Exception { 281 | 282 | if (relationshipIdToToken.isEmpty()) { 283 | return Map.of(); 284 | } 285 | 286 | var query = getRelationshipPropertiesQuery(config); 287 | 288 | var relationshipObjectTypes = new LinkedHashMap(); 289 | 290 | transaction.execute(query).accept((Result.ResultVisitor) resultRow -> { 291 | var relType = resultRow.getString("relType"); 292 | @SuppressWarnings("unchecked") 293 | var from = nodeObjectTypeIdGenerator.apply(":" + ((List) resultRow.get("from")).stream() 294 | .sorted() 295 | .map(v -> "`" + v + "`") 296 | .collect(Collectors.joining(":"))); 297 | @SuppressWarnings("unchecked") 298 | var to = nodeObjectTypeIdGenerator.apply(":" + ((List) resultRow.get("to")).stream() 299 | .sorted() 300 | .map(v -> "`" + v + "`") 301 | .collect(Collectors.joining(":"))); 302 | 303 | var id = new Ref(idGenerator.apply(relType, to)); 304 | var relationshipObject = relationshipObjectTypes.computeIfAbsent(id, key -> 305 | new RelationshipObjectType(key.value, new Ref(relationshipIdToToken.get(relType).id()), new Ref(from), new Ref(to))); 306 | extractProperty(resultRow) 307 | .ifPresent(relationshipObject.properties()::add); 308 | 309 | return true; 310 | }); 311 | return relationshipObjectTypes; 312 | } 313 | 314 | Optional extractProperty(Result.ResultRow resultRow) { 315 | var propertyName = resultRow.getString("propertyName"); 316 | if (propertyName == null) { 317 | return Optional.empty(); 318 | } 319 | 320 | @SuppressWarnings("unchecked") 321 | var types = ((List) resultRow.get("propertyTypes")).stream() 322 | .map(t -> { 323 | String type; 324 | String itemType = null; 325 | if (t.endsWith("Array")) { 326 | type = "array"; 327 | itemType = t.replace("Array", ""); 328 | itemType = TYPE_MAPPING.getOrDefault(itemType, itemType).toLowerCase(Locale.ROOT); 329 | } else { 330 | type = TYPE_MAPPING.getOrDefault(t, t).toLowerCase(Locale.ROOT); 331 | } 332 | return new GraphSchema.Type(type, itemType); 333 | }).toList(); 334 | 335 | return Optional.of(new GraphSchema.Property(propertyName, types, resultRow.getBoolean("mandatory"))); 336 | } 337 | 338 | private static String splitStripAndJoin(String value, String prefix) { 339 | return Arrays.stream(value.split(":")) 340 | .map(String::trim) 341 | .filter(Predicate.not(String::isBlank)) 342 | .map(t -> ENCLOSING_TICK_MARKS.matcher(t).replaceAll(m -> m.group(1))) 343 | .collect(Collectors.joining(":", prefix + ":", "")); 344 | } 345 | 346 | private static class NodeObjectIdGenerator implements UnaryOperator { 347 | 348 | private final boolean useConstantIds; 349 | 350 | NodeObjectIdGenerator(boolean useConstantIds) { 351 | this.useConstantIds = useConstantIds; 352 | } 353 | 354 | @Override 355 | public String apply(String nodeType) { 356 | 357 | if (useConstantIds) { 358 | return splitStripAndJoin(nodeType, "n"); 359 | } 360 | 361 | return ID_GENERATOR.get(); 362 | } 363 | } 364 | 365 | /** 366 | * Not thread safe. 367 | */ 368 | private static class RelationshipObjectIdGenerator implements BinaryOperator { 369 | 370 | private final boolean useConstantIds; 371 | private final Map> counter = new HashMap<>(); 372 | 373 | RelationshipObjectIdGenerator(boolean useConstantIds) { 374 | this.useConstantIds = useConstantIds; 375 | } 376 | 377 | @Override 378 | public String apply(String relType, String target) { 379 | 380 | if (useConstantIds) { 381 | var id = splitStripAndJoin(relType, "r"); 382 | var count = counter.computeIfAbsent(id, ignored -> new HashMap<>()); 383 | if (count.isEmpty()) { 384 | count.put(target, 0); 385 | return id; 386 | } else if (count.containsKey(target)) { 387 | var value = count.get(target); 388 | return value == 0 ? id : id + "_" + value; 389 | } else { 390 | var newValue = count.size(); 391 | count.put(target, newValue); 392 | return id + "_" + newValue; 393 | } 394 | } 395 | 396 | return ID_GENERATOR.get(); 397 | } 398 | } 399 | 400 | /** 401 | * Not thread safe. 402 | */ 403 | private static class CachingUnaryOperator implements UnaryOperator { 404 | 405 | private final Map cache = new HashMap<>(); 406 | private final UnaryOperator delegate; 407 | 408 | CachingUnaryOperator(UnaryOperator delegate) { 409 | this.delegate = delegate; 410 | } 411 | 412 | @Override 413 | public T apply(T s) { 414 | return cache.computeIfAbsent(s, delegate); 415 | } 416 | } 417 | } 418 | } 419 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/graph_schema/introspector/GraphSchemaGraphyResultWrapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 "Neo4j," 3 | * Neo4j Sweden AB [https://neo4j.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | package org.neo4j.graph_schema.introspector; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Arrays; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Set; 27 | import java.util.concurrent.atomic.AtomicLong; 28 | import java.util.function.Supplier; 29 | import java.util.stream.Collectors; 30 | 31 | import org.neo4j.graph_schema.introspector.GraphSchema.NodeObjectType; 32 | import org.neo4j.graph_schema.introspector.GraphSchema.Ref; 33 | import org.neo4j.graph_schema.introspector.GraphSchema.RelationshipObjectType; 34 | import org.neo4j.graph_schema.introspector.GraphSchema.Token; 35 | import org.neo4j.graphdb.Direction; 36 | import org.neo4j.graphdb.Entity; 37 | import org.neo4j.graphdb.Label; 38 | import org.neo4j.graphdb.Node; 39 | import org.neo4j.graphdb.Relationship; 40 | import org.neo4j.graphdb.RelationshipType; 41 | import org.neo4j.graphdb.ResourceIterable; 42 | 43 | /** 44 | * A graph-y result build from a {@link GraphSchema}. The wrapper is needed to make the Neo4j embedded API happy. 45 | */ 46 | public final class GraphSchemaGraphyResultWrapper { 47 | 48 | static GraphSchemaGraphyResultWrapper flat(GraphSchema graphSchema) { 49 | 50 | var nodeLabels = graphSchema.nodeLabels().values().stream() 51 | .collect(Collectors.toMap(Token::id, Token::value)); 52 | var relationshipTypes = graphSchema.relationshipTypes().values().stream() 53 | .collect(Collectors.toMap(Token::id, Token::value)); 54 | 55 | var result = new GraphSchemaGraphyResultWrapper(); 56 | 57 | var nodeObjectTypeNodes = new HashMap(); 58 | for (NodeObjectType nodeObjectType : graphSchema.nodeObjectTypes().values()) { 59 | var labels = nodeObjectType.labels().stream() 60 | .map(v -> nodeLabels.get(v.value())) 61 | .toArray(String[]::new); 62 | var node = new VirtualNode(Map.of("$id", nodeObjectType.id(), "name", labels.length == 0 ? "n/a" : labels[0], "properties", GraphSchemaModule.asJsonString(nodeObjectType.properties())), labels); 63 | nodeObjectTypeNodes.put(new Ref(nodeObjectType.id()), node); 64 | } 65 | result.nodes.addAll(nodeObjectTypeNodes.values()); 66 | 67 | for (RelationshipObjectType relationshipObjectType : graphSchema.relationshipObjectTypes().values()) { 68 | var relationship = new VirtualRelationship( 69 | nodeObjectTypeNodes.get(relationshipObjectType.from()), 70 | relationshipTypes.get(relationshipObjectType.type().value()), 71 | Map.of("$id", relationshipObjectType.id(), "properties", GraphSchemaModule.asJsonString(relationshipObjectType.properties())), 72 | nodeObjectTypeNodes.get(relationshipObjectType.to()) 73 | ); 74 | result.relationships.add(relationship); 75 | } 76 | 77 | return result; 78 | } 79 | 80 | static GraphSchemaGraphyResultWrapper full(GraphSchema graphSchema) { 81 | 82 | var nodeLabelNodes = graphSchema.nodeLabels().values().stream() 83 | .collect(Collectors.toMap(Token::id, t -> toVirtualNode(t, "NodeLabel"))); 84 | var relationshipTypeNodes = graphSchema.relationshipTypes().values().stream() 85 | .collect(Collectors.toMap(Token::id, t -> toVirtualNode(t, "RelationshipType"))); 86 | 87 | var result = new GraphSchemaGraphyResultWrapper(); 88 | 89 | var nodeObjectTypeNodes = new HashMap(); 90 | for (var entry : graphSchema.nodeObjectTypes().entrySet()) { 91 | var nodeObjectType = entry.getValue(); 92 | var node = new VirtualNode(Map.of("$id", nodeObjectType.id(), "properties", GraphSchemaModule.asJsonString(nodeObjectType.properties())), "NodeObjectType"); 93 | nodeObjectTypeNodes.put(entry.getKey(), node); 94 | 95 | for (var ref : nodeObjectType.labels()) { 96 | var tokenNode = nodeLabelNodes.get(ref.value()); 97 | result.relationships.add(new VirtualRelationship(node, "HAS_LABEL", tokenNode)); 98 | } 99 | } 100 | 101 | for (RelationshipObjectType relationshipObjectType : graphSchema.relationshipObjectTypes().values()) { 102 | var node = new VirtualNode(Map.of("$id", relationshipObjectType.id(), "properties", GraphSchemaModule.asJsonString(relationshipObjectType.properties())), "RelationshipObjectTypes"); 103 | result.nodes.add(node); 104 | result.relationships.add(new VirtualRelationship(node, "HAS_TYPE", relationshipTypeNodes.get(relationshipObjectType.type().value()))); 105 | result.relationships.add(new VirtualRelationship(node, "FROM", nodeObjectTypeNodes.get(relationshipObjectType.from()))); 106 | result.relationships.add(new VirtualRelationship(node, "TO", nodeObjectTypeNodes.get(relationshipObjectType.to()))); 107 | } 108 | 109 | // Add remaining things 110 | result.nodes.addAll(nodeLabelNodes.values()); 111 | result.nodes.addAll(relationshipTypeNodes.values()); 112 | result.nodes.addAll(nodeObjectTypeNodes.values()); 113 | 114 | return result; 115 | } 116 | 117 | private static Node toVirtualNode(Token token, String label) { 118 | 119 | return new VirtualNode(Map.of("$id", token.id(), "value", token.value()), "Token", label); 120 | } 121 | 122 | // Public field required for Neo4j internal API. 123 | public final List nodes = new ArrayList<>(); 124 | public final List relationships = new ArrayList<>(); 125 | 126 | /** 127 | * A virtual entity, spotting a negative ID and a random element id. 128 | */ 129 | abstract static class VirtualEntity implements Entity { 130 | 131 | private static final Supplier ID_FACTORY = new AtomicLong(-1)::decrementAndGet; 132 | 133 | private final long id; 134 | 135 | private final String elementId; 136 | 137 | private final Map properties; 138 | 139 | VirtualEntity(Map properties) { 140 | this.id = ID_FACTORY.get(); 141 | this.elementId = String.valueOf(this.id); 142 | this.properties = Map.copyOf(properties); 143 | } 144 | 145 | @SuppressWarnings("removal") 146 | @Override 147 | public final long getId() { 148 | return id; 149 | } 150 | 151 | @Override 152 | public final String getElementId() { 153 | return elementId; 154 | } 155 | 156 | @Override 157 | public final boolean hasProperty(String key) { 158 | return properties.containsKey(key); 159 | } 160 | 161 | @Override 162 | public final Object getProperty(String key) { 163 | return properties.get(key); 164 | } 165 | 166 | @Override 167 | public final Object getProperty(String key, Object defaultValue) { 168 | return properties.getOrDefault(key, defaultValue); 169 | } 170 | 171 | @Override 172 | public final void setProperty(String key, Object value) { 173 | throw new UnsupportedOperationException(); 174 | } 175 | 176 | @Override 177 | public final Object removeProperty(String key) { 178 | throw new UnsupportedOperationException(); 179 | } 180 | 181 | @Override 182 | public final Iterable getPropertyKeys() { 183 | return properties.keySet(); 184 | } 185 | 186 | @Override 187 | public final Map getProperties(String... keys) { 188 | Map result = new HashMap<>(); 189 | for (String key : keys) { 190 | if (!hasProperty(key)) { 191 | continue; 192 | } 193 | result.put(key, getProperty(key)); 194 | } 195 | return result; 196 | } 197 | 198 | @Override 199 | public final Map getAllProperties() { 200 | return properties; 201 | } 202 | 203 | @Override 204 | public final void delete() { 205 | throw new UnsupportedOperationException(); 206 | } 207 | } 208 | 209 | static final class VirtualNode extends VirtualEntity implements Node { 210 | 211 | private final Set

37 | * The instrospector creates JSON ids based on the labels and types by default. It can alternatively use 38 | * Time-Sorted Unique Identifiers (TSID) for the ids inside the generated schema by calling it via 39 | * {@code CALL experimental.introspect.asJson({useConstantIds: false})} 40 | */ 41 | public class Introspect { 42 | 43 | @Context 44 | public Transaction transaction; 45 | 46 | /** 47 | * Shared configuration of the functions and procedures in this class. 48 | * 49 | * @param useConstantIds Whether to use constant ids (derived from tokens) or generate ids 50 | * @param prettyPrint Whether to pretty print the result or not 51 | * @param quoteTokens Whether to always quote tokens or not 52 | * @param sampleOnly Whether to sample relationships for determining properties on concrete relationships or not (defaults to {@literal true}) 53 | */ 54 | record Config(boolean useConstantIds, boolean prettyPrint, boolean quoteTokens, boolean sampleOnly) { 55 | 56 | Config(Map params) { 57 | this( 58 | (boolean) params.getOrDefault("useConstantIds", true), 59 | (boolean) params.getOrDefault("prettyPrint", false), 60 | (boolean) params.getOrDefault("quoteTokens", true), 61 | (boolean) params.getOrDefault("sampleOnly", true) 62 | ); 63 | } 64 | } 65 | 66 | @Procedure(name = "experimental.introspect.asJson", mode = Mode.READ) 67 | @Description("" + 68 | "Call with {useConstantIds: false} to generate substitute ids for all tokens and use {prettyPrint: true} for enabling pretty printing;" + 69 | "{quoteTokens: false} will disable quotation of tokens.") 70 | public Stream introspectAsJson(@Name("params") Map params) throws Exception { 71 | 72 | var config = new Config(params); 73 | var graphSchema = GraphSchema.build(transaction, config); 74 | 75 | return Stream.of(GraphSchemaJSONResultWrapper.of(graphSchema, config)); 76 | } 77 | 78 | @Procedure(name = "experimental.introspect.asGraph", mode = Mode.READ) 79 | @Description("Visualizes the JSON generated by this introspector in a graphy way") 80 | public Stream introspectAndVisualize(@Name("params") Map params) throws Exception { 81 | 82 | var graphSchema = GraphSchema.build(transaction, new Config(params)); 83 | var flat = (boolean) params.getOrDefault("flat", false); 84 | return Stream.of(flat ? GraphSchemaGraphyResultWrapper.flat(graphSchema) : GraphSchemaGraphyResultWrapper.full(graphSchema)); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/org/neo4j/graph_schema/introspector/GraphSchemaTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 "Neo4j," 3 | * Neo4j Sweden AB [https://neo4j.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | package org.neo4j.graph_schema.introspector; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | 23 | import java.lang.reflect.InvocationTargetException; 24 | import java.util.Map; 25 | 26 | import org.junit.jupiter.api.Nested; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.platform.commons.util.ReflectionUtils; 29 | 30 | /** 31 | * @author Michael J. Simons 32 | */ 33 | class GraphSchemaTest { 34 | 35 | @Nested 36 | class IntrospectTest { 37 | 38 | @Test 39 | void shouldSampleByDefault() throws InvocationTargetException, IllegalAccessException { 40 | 41 | var getRelationshipPropertiesQuery = ReflectionUtils.getRequiredMethod(GraphSchema.Introspector.class, "getRelationshipPropertiesQuery", Introspect.Config.class); 42 | getRelationshipPropertiesQuery.setAccessible(true); 43 | var query = getRelationshipPropertiesQuery.invoke(null, new Introspect.Config(Map.of())); 44 | assertThat(query).isEqualTo(""" 45 | CALL db.schema.relTypeProperties() YIELD relType, propertyName, propertyTypes, mandatory 46 | WITH substring(relType, 2, size(relType)-3) AS relType, propertyName, propertyTypes, mandatory 47 | CALL { 48 | WITH relType, propertyName 49 | MATCH (n)-[r]->(m) WHERE type(r) = relType AND (r[propertyName] IS NOT NULL OR propertyName IS NULL) 50 | WITH n, r, m 51 | LIMIT 100 52 | WITH DISTINCT labels(n) AS from, labels(m) AS to 53 | RETURN from, to 54 | } 55 | RETURN DISTINCT from, to, relType, propertyName, propertyTypes, mandatory 56 | ORDER BY relType ASC 57 | """); 58 | } 59 | 60 | @Test 61 | void sampleCanBeDisabled() throws InvocationTargetException, IllegalAccessException { 62 | 63 | var getRelationshipPropertiesQuery = ReflectionUtils.getRequiredMethod(GraphSchema.Introspector.class, "getRelationshipPropertiesQuery", Introspect.Config.class); 64 | getRelationshipPropertiesQuery.setAccessible(true); 65 | var query = getRelationshipPropertiesQuery.invoke(null, new Introspect.Config(Map.of("sampleOnly", false))); 66 | assertThat(query).isEqualTo(""" 67 | CALL db.schema.relTypeProperties() YIELD relType, propertyName, propertyTypes, mandatory 68 | WITH substring(relType, 2, size(relType)-3) AS relType, propertyName, propertyTypes, mandatory 69 | CALL { 70 | WITH relType, propertyName 71 | MATCH (n)-[r]->(m) WHERE type(r) = relType AND (r[propertyName] IS NOT NULL OR propertyName IS NULL) 72 | WITH n, r, m 73 | // LIMIT 74 | WITH DISTINCT labels(n) AS from, labels(m) AS to 75 | RETURN from, to 76 | } 77 | RETURN DISTINCT from, to, relType, propertyName, propertyTypes, mandatory 78 | ORDER BY relType ASC 79 | """); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/org/neo4j/graph_schema/introspector/IntrospectTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 "Neo4j," 3 | * Neo4j Sweden AB [https://neo4j.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | package org.neo4j.graph_schema.introspector; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.assertj.core.api.Assertions.assertThatNoException; 23 | 24 | import java.io.BufferedReader; 25 | import java.io.IOException; 26 | import java.io.InputStreamReader; 27 | import java.io.StringWriter; 28 | 29 | import org.junit.jupiter.api.AfterAll; 30 | import org.junit.jupiter.api.BeforeAll; 31 | import org.junit.jupiter.api.Test; 32 | import org.junit.jupiter.api.TestInstance; 33 | import org.neo4j.driver.GraphDatabase; 34 | import org.neo4j.harness.Neo4j; 35 | import org.neo4j.harness.Neo4jBuilders; 36 | 37 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 38 | class IntrospectTest { 39 | 40 | private Neo4j embeddedDatabaseServer; 41 | 42 | @BeforeAll 43 | void initializeNeo4j() throws IOException { 44 | 45 | var sw = new StringWriter(); 46 | try (var in = new BufferedReader(new InputStreamReader(getClass().getResourceAsStream("/movie.cypher")))) { 47 | in.transferTo(sw); 48 | sw.flush(); 49 | } 50 | 51 | this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() 52 | .withDisabledServer() 53 | // language=cypher 54 | .withFixture(""" 55 | UNWIND range(1, 5) AS i 56 | WITH i CREATE (n:SomeNode {idx: i}) 57 | """) 58 | // language=cypher 59 | .withFixture(""" 60 | CREATE (:Actor:Person {name: 'Weird Id1', id: 'abc'}) 61 | CREATE (:Actor:Person {name: 'Weird Id2', id: 4711}) 62 | CREATE (:Actor:Person {name: 'Weird Id3', id: ["pt1", "pt2"]}) 63 | CREATE (:Actor:Person {name: 'Weird Id4', id: [21, 23, 42]}) 64 | CREATE (:Actor:Person {name: 'A compromise id', id: [0.5]}) 65 | CREATE (:Actor:Person {name: 'A number', f: 0.5}) 66 | CREATE (:Actor:Person {name: 'Another number', f: 50}) 67 | CREATE (:Actor:Person {name: 'A point', p: point({latitude:toFloat('13.43'), longitude:toFloat('56.21')})}) 68 | CREATE (:L1:`L ``2`) - [:RELATED_TO {since: datetime()}] -> (:L2:L3) 69 | CREATE (:L1:L2) - [:RELATED_TO {since: datetime()}] -> (:L2:L3) 70 | CREATE (:L1:L2) - [:RELATED_TO {since: datetime()}] -> (:Unrelated) 71 | CREATE (:Person) -[:REVIEWED] ->(:Book) 72 | CREATE (a:Wurst) -[:```HAT_DEN`] -> (:Salat) 73 | """) 74 | .withProcedure(Introspect.class) 75 | .build(); 76 | } 77 | 78 | @AfterAll 79 | void closeNeo4j() { 80 | this.embeddedDatabaseServer.close(); 81 | } 82 | 83 | @Test 84 | void smokeTest() { 85 | 86 | // language=json 87 | var expected = """ 88 | { 89 | "graphSchemaRepresentation" : { 90 | "graphSchema" : { 91 | "nodeLabels" : [ { 92 | "$id" : "nl:L1", 93 | "token" : "L1" 94 | }, { 95 | "$id" : "nl:L `2", 96 | "token" : "`L ``2`" 97 | }, { 98 | "$id" : "nl:Wurst", 99 | "token" : "Wurst" 100 | }, { 101 | "$id" : "nl:L2", 102 | "token" : "L2" 103 | }, { 104 | "$id" : "nl:Book", 105 | "token" : "Book" 106 | }, { 107 | "$id" : "nl:Actor", 108 | "token" : "Actor" 109 | }, { 110 | "$id" : "nl:L3", 111 | "token" : "L3" 112 | }, { 113 | "$id" : "nl:Unrelated", 114 | "token" : "Unrelated" 115 | }, { 116 | "$id" : "nl:Person", 117 | "token" : "Person" 118 | }, { 119 | "$id" : "nl:Salat", 120 | "token" : "Salat" 121 | }, { 122 | "$id" : "nl:SomeNode", 123 | "token" : "SomeNode" 124 | } ], 125 | "relationshipTypes" : [ { 126 | "$id" : "rt:`HAT_DEN", 127 | "token" : "```HAT_DEN`" 128 | }, { 129 | "$id" : "rt:REVIEWED", 130 | "token" : "REVIEWED" 131 | }, { 132 | "$id" : "rt:RELATED_TO", 133 | "token" : "RELATED_TO" 134 | } ], 135 | "nodeObjectTypes" : [ { 136 | "labels" : [ { 137 | "$ref" : "#nl:Actor" 138 | }, { 139 | "$ref" : "#nl:Person" 140 | } ], 141 | "properties" : [ { 142 | "token" : "name", 143 | "type" : { 144 | "type" : "string" 145 | }, 146 | "nullable" : false 147 | }, { 148 | "token" : "id", 149 | "type" : [ { 150 | "type" : "array", 151 | "items" : { 152 | "type" : "integer" 153 | } 154 | }, { 155 | "type" : "array", 156 | "items" : { 157 | "type" : "string" 158 | } 159 | }, { 160 | "type" : "integer" 161 | }, { 162 | "type" : "string" 163 | }, { 164 | "type" : "array", 165 | "items" : { 166 | "type" : "float" 167 | } 168 | } ], 169 | "nullable" : true 170 | }, { 171 | "token" : "f", 172 | "type" : [ { 173 | "type" : "integer" 174 | }, { 175 | "type" : "float" 176 | } ], 177 | "nullable" : true 178 | }, { 179 | "token" : "p", 180 | "type" : { 181 | "type" : "point" 182 | }, 183 | "nullable" : true 184 | } ], 185 | "$id" : "n:Actor:Person" 186 | }, { 187 | "labels" : [ { 188 | "$ref" : "#nl:Book" 189 | } ], 190 | "properties" : [ ], 191 | "$id" : "n:Book" 192 | }, { 193 | "labels" : [ { 194 | "$ref" : "#nl:L `2" 195 | }, { 196 | "$ref" : "#nl:L1" 197 | } ], 198 | "properties" : [ ], 199 | "$id" : "n:L `2:L1" 200 | }, { 201 | "labels" : [ { 202 | "$ref" : "#nl:L1" 203 | }, { 204 | "$ref" : "#nl:L2" 205 | } ], 206 | "properties" : [ ], 207 | "$id" : "n:L1:L2" 208 | }, { 209 | "labels" : [ { 210 | "$ref" : "#nl:L2" 211 | }, { 212 | "$ref" : "#nl:L3" 213 | } ], 214 | "properties" : [ ], 215 | "$id" : "n:L2:L3" 216 | }, { 217 | "labels" : [ { 218 | "$ref" : "#nl:Person" 219 | } ], 220 | "properties" : [ ], 221 | "$id" : "n:Person" 222 | }, { 223 | "labels" : [ { 224 | "$ref" : "#nl:Salat" 225 | } ], 226 | "properties" : [ ], 227 | "$id" : "n:Salat" 228 | }, { 229 | "labels" : [ { 230 | "$ref" : "#nl:SomeNode" 231 | } ], 232 | "properties" : [ { 233 | "token" : "idx", 234 | "type" : { 235 | "type" : "integer" 236 | }, 237 | "nullable" : false 238 | } ], 239 | "$id" : "n:SomeNode" 240 | }, { 241 | "labels" : [ { 242 | "$ref" : "#nl:Unrelated" 243 | } ], 244 | "properties" : [ ], 245 | "$id" : "n:Unrelated" 246 | }, { 247 | "labels" : [ { 248 | "$ref" : "#nl:Wurst" 249 | } ], 250 | "properties" : [ ], 251 | "$id" : "n:Wurst" 252 | } ], 253 | "relationshipObjectTypes" : [ { 254 | "type" : { 255 | "$ref" : "#rt:RELATED_TO" 256 | }, 257 | "from" : { 258 | "$ref" : "#n:L `2:L1" 259 | }, 260 | "to" : { 261 | "$ref" : "#n:L2:L3" 262 | }, 263 | "properties" : [ { 264 | "token" : "since", 265 | "type" : { 266 | "type" : "datetime" 267 | }, 268 | "nullable" : false 269 | }, { 270 | "token" : "since", 271 | "type" : { 272 | "type" : "datetime" 273 | }, 274 | "nullable" : false 275 | } ], 276 | "$id" : "r:RELATED_TO" 277 | }, { 278 | "type" : { 279 | "$ref" : "#rt:RELATED_TO" 280 | }, 281 | "from" : { 282 | "$ref" : "#n:L1:L2" 283 | }, 284 | "to" : { 285 | "$ref" : "#n:Unrelated" 286 | }, 287 | "properties" : [ { 288 | "token" : "since", 289 | "type" : { 290 | "type" : "datetime" 291 | }, 292 | "nullable" : false 293 | } ], 294 | "$id" : "r:RELATED_TO_1" 295 | }, { 296 | "type" : { 297 | "$ref" : "#rt:REVIEWED" 298 | }, 299 | "from" : { 300 | "$ref" : "#n:Person" 301 | }, 302 | "to" : { 303 | "$ref" : "#n:Book" 304 | }, 305 | "properties" : [ ], 306 | "$id" : "r:REVIEWED" 307 | }, { 308 | "type" : { 309 | "$ref" : "#rt:`HAT_DEN" 310 | }, 311 | "from" : { 312 | "$ref" : "#n:Wurst" 313 | }, 314 | "to" : { 315 | "$ref" : "#n:Salat" 316 | }, 317 | "properties" : [ ], 318 | "$id" : "r:`HAT_DEN" 319 | } ] 320 | } 321 | } 322 | }"""; 323 | 324 | try ( 325 | var driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI()); 326 | var session = driver.session() 327 | ) { 328 | 329 | var result = session.run("CALL experimental.introspect.asJson({useConstantIds: true, prettyPrint: true}) YIELD value RETURN value AS result").single().get("result").asString(); 330 | assertThat(result).isEqualTo(expected); 331 | 332 | var graphSchemaObjectMapper = GraphSchemaModule.getGraphSchemaObjectMapper(); 333 | assertThatNoException().isThrownBy(() -> graphSchemaObjectMapper.readValue(result, GraphSchema.class)); 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /src/test/java/org/neo4j/graph_schema/introspector/SamplingTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 "Neo4j," 3 | * Neo4j Sweden AB [https://neo4j.com] 4 | * 5 | * This file is part of Neo4j. 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * https://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | package org.neo4j.graph_schema.introspector; 20 | 21 | import static org.assertj.core.api.Assertions.assertThat; 22 | import static org.assertj.core.api.Assertions.assertThatNoException; 23 | 24 | import org.junit.jupiter.api.AfterAll; 25 | import org.junit.jupiter.api.BeforeAll; 26 | import org.junit.jupiter.api.Test; 27 | import org.junit.jupiter.api.TestInstance; 28 | import org.neo4j.driver.GraphDatabase; 29 | import org.neo4j.harness.Neo4j; 30 | import org.neo4j.harness.Neo4jBuilders; 31 | 32 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 33 | class SamplingTest { 34 | 35 | private Neo4j embeddedDatabaseServer; 36 | 37 | @BeforeAll 38 | void initializeNeo4j() { 39 | 40 | this.embeddedDatabaseServer = Neo4jBuilders.newInProcessBuilder() 41 | .withDisabledServer() 42 | .withFixture( 43 | "CREATE (:A1) -[:A_TYPE {x: 'hallo'}]-> (:B1)\n".repeat((int) (GraphSchema.Introspector.DEFAULT_SAMPLE_SIZE * 10)) + 44 | "CREATE (:A2) -[:A_TYPE {x: 'hallo'}]-> (:B2)\n" 45 | ) 46 | .withProcedure(Introspect.class) 47 | .build(); 48 | } 49 | 50 | @AfterAll 51 | void closeNeo4j() { 52 | this.embeddedDatabaseServer.close(); 53 | } 54 | 55 | @Test 56 | void samplingShouldWork() { 57 | 58 | 59 | 60 | try ( 61 | var driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI()); 62 | var session = driver.session() 63 | ) { 64 | 65 | var result = session.run("CALL experimental.introspect.asJson({useConstantIds: true, prettyPrint: true}) YIELD value RETURN value AS result").single().get("result").asString(); 66 | } 67 | } 68 | 69 | @Test 70 | void samplingCanBeDisabled() { 71 | 72 | // language=json 73 | var expected = """ 74 | { 75 | "graphSchemaRepresentation" : { 76 | "graphSchema" : { 77 | "nodeLabels" : [ { 78 | "$id" : "nl:A1", 79 | "token" : "A1" 80 | }, { 81 | "$id" : "nl:B2", 82 | "token" : "B2" 83 | }, { 84 | "$id" : "nl:A2", 85 | "token" : "A2" 86 | }, { 87 | "$id" : "nl:B1", 88 | "token" : "B1" 89 | } ], 90 | "relationshipTypes" : [ { 91 | "$id" : "rt:A_TYPE", 92 | "token" : "A_TYPE" 93 | } ], 94 | "nodeObjectTypes" : [ { 95 | "labels" : [ { 96 | "$ref" : "#nl:A1" 97 | } ], 98 | "properties" : [ ], 99 | "$id" : "n:A1" 100 | }, { 101 | "labels" : [ { 102 | "$ref" : "#nl:A2" 103 | } ], 104 | "properties" : [ ], 105 | "$id" : "n:A2" 106 | }, { 107 | "labels" : [ { 108 | "$ref" : "#nl:B1" 109 | } ], 110 | "properties" : [ ], 111 | "$id" : "n:B1" 112 | }, { 113 | "labels" : [ { 114 | "$ref" : "#nl:B2" 115 | } ], 116 | "properties" : [ ], 117 | "$id" : "n:B2" 118 | } ], 119 | "relationshipObjectTypes" : [ { 120 | "type" : { 121 | "$ref" : "#rt:A_TYPE" 122 | }, 123 | "from" : { 124 | "$ref" : "#n:A1" 125 | }, 126 | "to" : { 127 | "$ref" : "#n:B1" 128 | }, 129 | "properties" : [ { 130 | "token" : "x", 131 | "type" : { 132 | "type" : "string" 133 | }, 134 | "nullable" : false 135 | } ], 136 | "$id" : "r:A_TYPE" 137 | }, { 138 | "type" : { 139 | "$ref" : "#rt:A_TYPE" 140 | }, 141 | "from" : { 142 | "$ref" : "#n:A2" 143 | }, 144 | "to" : { 145 | "$ref" : "#n:B2" 146 | }, 147 | "properties" : [ { 148 | "token" : "x", 149 | "type" : { 150 | "type" : "string" 151 | }, 152 | "nullable" : false 153 | } ], 154 | "$id" : "r:A_TYPE_1" 155 | } ] 156 | } 157 | } 158 | }"""; 159 | 160 | try ( 161 | var driver = GraphDatabase.driver(embeddedDatabaseServer.boltURI()); 162 | var session = driver.session() 163 | ) { 164 | 165 | var result = session.run("CALL experimental.introspect.asJson({useConstantIds: true, prettyPrint: true, sampleOnly: false}) YIELD value RETURN value AS result").single().get("result").asString(); 166 | assertThat(result).isEqualTo(expected); 167 | 168 | var graphSchemaObjectMapper = GraphSchemaModule.getGraphSchemaObjectMapper(); 169 | assertThatNoException().isThrownBy(() -> graphSchemaObjectMapper.readValue(result, GraphSchema.class)); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/resources/movie.cypher: -------------------------------------------------------------------------------- 1 | CREATE (TheMatrix:Movie {title:'The Matrix', released:1999, tagline:'Welcome to the Real World'}) 2 | CREATE (Keanu:Person {name:'Keanu Reeves', born:1964}) 3 | CREATE (Carrie:Person {name:'Carrie-Anne Moss', born:1967}) 4 | CREATE (Laurence:Person {name:'Laurence Fishburne', born:1961}) 5 | CREATE (Hugo:Person {name:'Hugo Weaving', born:1960}) 6 | CREATE (LillyW:Person {name:'Lilly Wachowski', born:1967}) 7 | CREATE (LanaW:Person {name:'Lana Wachowski', born:1965}) 8 | CREATE (JoelS:Person {name:'Joel Silver', born:1952}) 9 | CREATE 10 | (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrix), 11 | (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrix), 12 | (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrix), 13 | (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrix), 14 | (LillyW)-[:DIRECTED]->(TheMatrix), 15 | (LanaW)-[:DIRECTED]->(TheMatrix), 16 | (JoelS)-[:PRODUCED]->(TheMatrix) 17 | 18 | CREATE (Emil:Person {name:"Emil Eifrem", born:1978}) 19 | CREATE (Emil)-[:ACTED_IN {roles:["Emil"]}]->(TheMatrix) 20 | 21 | CREATE (TheMatrixReloaded:Movie {title:'The Matrix Reloaded', released:2003, tagline:'Free your mind'}) 22 | CREATE 23 | (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixReloaded), 24 | (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixReloaded), 25 | (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixReloaded), 26 | (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixReloaded), 27 | (LillyW)-[:DIRECTED]->(TheMatrixReloaded), 28 | (LanaW)-[:DIRECTED]->(TheMatrixReloaded), 29 | (JoelS)-[:PRODUCED]->(TheMatrixReloaded) 30 | 31 | CREATE (TheMatrixRevolutions:Movie {title:'The Matrix Revolutions', released:2003, tagline:'Everything that has a beginning has an end'}) 32 | CREATE 33 | (Keanu)-[:ACTED_IN {roles:['Neo']}]->(TheMatrixRevolutions), 34 | (Carrie)-[:ACTED_IN {roles:['Trinity']}]->(TheMatrixRevolutions), 35 | (Laurence)-[:ACTED_IN {roles:['Morpheus']}]->(TheMatrixRevolutions), 36 | (Hugo)-[:ACTED_IN {roles:['Agent Smith']}]->(TheMatrixRevolutions), 37 | (LillyW)-[:DIRECTED]->(TheMatrixRevolutions), 38 | (LanaW)-[:DIRECTED]->(TheMatrixRevolutions), 39 | (JoelS)-[:PRODUCED]->(TheMatrixRevolutions) 40 | 41 | CREATE (TheDevilsAdvocate:Movie {title:"The Devil's Advocate", released:1997, tagline:'Evil has its winning ways'}) 42 | CREATE (Charlize:Person {name:'Charlize Theron', born:1975}) 43 | CREATE (Al:Person {name:'Al Pacino', born:1940}) 44 | CREATE (Taylor:Person {name:'Taylor Hackford', born:1944}) 45 | CREATE 46 | (Keanu)-[:ACTED_IN {roles:['Kevin Lomax']}]->(TheDevilsAdvocate), 47 | (Charlize)-[:ACTED_IN {roles:['Mary Ann Lomax']}]->(TheDevilsAdvocate), 48 | (Al)-[:ACTED_IN {roles:['John Milton']}]->(TheDevilsAdvocate), 49 | (Taylor)-[:DIRECTED]->(TheDevilsAdvocate) 50 | 51 | CREATE (AFewGoodMen:Movie {title:"A Few Good Men", released:1992, tagline:"In the heart of the nation's capital, in a courthouse of the U.S. government, one man will stop at nothing to keep his honor, and one will stop at nothing to find the truth."}) 52 | CREATE (TomC:Person {name:'Tom Cruise', born:1962}) 53 | CREATE (JackN:Person {name:'Jack Nicholson', born:1937}) 54 | CREATE (DemiM:Person {name:'Demi Moore', born:1962}) 55 | CREATE (KevinB:Person {name:'Kevin Bacon', born:1958}) 56 | CREATE (KieferS:Person {name:'Kiefer Sutherland', born:1966}) 57 | CREATE (NoahW:Person {name:'Noah Wyle', born:1971}) 58 | CREATE (CubaG:Person {name:'Cuba Gooding Jr.', born:1968}) 59 | CREATE (KevinP:Person {name:'Kevin Pollak', born:1957}) 60 | CREATE (JTW:Person {name:'J.T. Walsh', born:1943}) 61 | CREATE (JamesM:Person {name:'James Marshall', born:1967}) 62 | CREATE (ChristopherG:Person {name:'Christopher Guest', born:1948}) 63 | CREATE (RobR:Person {name:'Rob Reiner', born:1947}) 64 | CREATE (AaronS:Person {name:'Aaron Sorkin', born:1961}) 65 | CREATE 66 | (TomC)-[:ACTED_IN {roles:['Lt. Daniel Kaffee']}]->(AFewGoodMen), 67 | (JackN)-[:ACTED_IN {roles:['Col. Nathan R. Jessup']}]->(AFewGoodMen), 68 | (DemiM)-[:ACTED_IN {roles:['Lt. Cdr. JoAnne Galloway']}]->(AFewGoodMen), 69 | (KevinB)-[:ACTED_IN {roles:['Capt. Jack Ross']}]->(AFewGoodMen), 70 | (KieferS)-[:ACTED_IN {roles:['Lt. Jonathan Kendrick']}]->(AFewGoodMen), 71 | (NoahW)-[:ACTED_IN {roles:['Cpl. Jeffrey Barnes']}]->(AFewGoodMen), 72 | (CubaG)-[:ACTED_IN {roles:['Cpl. Carl Hammaker']}]->(AFewGoodMen), 73 | (KevinP)-[:ACTED_IN {roles:['Lt. Sam Weinberg']}]->(AFewGoodMen), 74 | (JTW)-[:ACTED_IN {roles:['Lt. Col. Matthew Andrew Markinson']}]->(AFewGoodMen), 75 | (JamesM)-[:ACTED_IN {roles:['Pfc. Louden Downey']}]->(AFewGoodMen), 76 | (ChristopherG)-[:ACTED_IN {roles:['Dr. Stone']}]->(AFewGoodMen), 77 | (AaronS)-[:ACTED_IN {roles:['Man in Bar']}]->(AFewGoodMen), 78 | (RobR)-[:DIRECTED]->(AFewGoodMen), 79 | (AaronS)-[:WROTE]->(AFewGoodMen) 80 | 81 | CREATE (TopGun:Movie {title:"Top Gun", released:1986, tagline:'I feel the need, the need for speed.'}) 82 | CREATE (KellyM:Person {name:'Kelly McGillis', born:1957}) 83 | CREATE (ValK:Person {name:'Val Kilmer', born:1959}) 84 | CREATE (AnthonyE:Person {name:'Anthony Edwards', born:1962}) 85 | CREATE (TomS:Person {name:'Tom Skerritt', born:1933}) 86 | CREATE (MegR:Person {name:'Meg Ryan', born:1961}) 87 | CREATE (TonyS:Person {name:'Tony Scott', born:1944}) 88 | CREATE (JimC:Person {name:'Jim Cash', born:1941}) 89 | CREATE 90 | (TomC)-[:ACTED_IN {roles:['Maverick']}]->(TopGun), 91 | (KellyM)-[:ACTED_IN {roles:['Charlie']}]->(TopGun), 92 | (ValK)-[:ACTED_IN {roles:['Iceman']}]->(TopGun), 93 | (AnthonyE)-[:ACTED_IN {roles:['Goose']}]->(TopGun), 94 | (TomS)-[:ACTED_IN {roles:['Viper']}]->(TopGun), 95 | (MegR)-[:ACTED_IN {roles:['Carole']}]->(TopGun), 96 | (TonyS)-[:DIRECTED]->(TopGun), 97 | (JimC)-[:WROTE]->(TopGun) 98 | 99 | CREATE (JerryMaguire:Movie {title:'Jerry Maguire', released:2000, tagline:'The rest of his life begins now.'}) 100 | CREATE (ReneeZ:Person {name:'Renee Zellweger', born:1969}) 101 | CREATE (KellyP:Person {name:'Kelly Preston', born:1962}) 102 | CREATE (JerryO:Person {name:"Jerry O'Connell", born:1974}) 103 | CREATE (JayM:Person {name:'Jay Mohr', born:1970}) 104 | CREATE (BonnieH:Person {name:'Bonnie Hunt', born:1961}) 105 | CREATE (ReginaK:Person {name:'Regina King', born:1971}) 106 | CREATE (JonathanL:Person {name:'Jonathan Lipnicki', born:1996}) 107 | CREATE (CameronC:Person {name:'Cameron Crowe', born:1957}) 108 | CREATE 109 | (TomC)-[:ACTED_IN {roles:['Jerry Maguire']}]->(JerryMaguire), 110 | (CubaG)-[:ACTED_IN {roles:['Rod Tidwell']}]->(JerryMaguire), 111 | (ReneeZ)-[:ACTED_IN {roles:['Dorothy Boyd']}]->(JerryMaguire), 112 | (KellyP)-[:ACTED_IN {roles:['Avery Bishop']}]->(JerryMaguire), 113 | (JerryO)-[:ACTED_IN {roles:['Frank Cushman']}]->(JerryMaguire), 114 | (JayM)-[:ACTED_IN {roles:['Bob Sugar']}]->(JerryMaguire), 115 | (BonnieH)-[:ACTED_IN {roles:['Laurel Boyd']}]->(JerryMaguire), 116 | (ReginaK)-[:ACTED_IN {roles:['Marcee Tidwell']}]->(JerryMaguire), 117 | (JonathanL)-[:ACTED_IN {roles:['Ray Boyd']}]->(JerryMaguire), 118 | (CameronC)-[:DIRECTED]->(JerryMaguire), 119 | (CameronC)-[:PRODUCED]->(JerryMaguire), 120 | (CameronC)-[:WROTE]->(JerryMaguire) 121 | 122 | CREATE (StandByMe:Movie {title:"Stand By Me", released:1986, tagline:"For some, it's the last real taste of innocence, and the first real taste of life. But for everyone, it's the time that memories are made of."}) 123 | CREATE (RiverP:Person {name:'River Phoenix', born:1970}) 124 | CREATE (CoreyF:Person {name:'Corey Feldman', born:1971}) 125 | CREATE (WilW:Person {name:'Wil Wheaton', born:1972}) 126 | CREATE (JohnC:Person {name:'John Cusack', born:1966}) 127 | CREATE (MarshallB:Person {name:'Marshall Bell', born:1942}) 128 | CREATE 129 | (WilW)-[:ACTED_IN {roles:['Gordie Lachance']}]->(StandByMe), 130 | (RiverP)-[:ACTED_IN {roles:['Chris Chambers']}]->(StandByMe), 131 | (JerryO)-[:ACTED_IN {roles:['Vern Tessio']}]->(StandByMe), 132 | (CoreyF)-[:ACTED_IN {roles:['Teddy Duchamp']}]->(StandByMe), 133 | (JohnC)-[:ACTED_IN {roles:['Denny Lachance']}]->(StandByMe), 134 | (KieferS)-[:ACTED_IN {roles:['Ace Merrill']}]->(StandByMe), 135 | (MarshallB)-[:ACTED_IN {roles:['Mr. Lachance']}]->(StandByMe), 136 | (RobR)-[:DIRECTED]->(StandByMe) 137 | 138 | CREATE (AsGoodAsItGets:Movie {title:'As Good as It Gets', released:1997, tagline:'A comedy from the heart that goes for the throat.'}) 139 | CREATE (HelenH:Person {name:'Helen Hunt', born:1963}) 140 | CREATE (GregK:Person {name:'Greg Kinnear', born:1963}) 141 | CREATE (JamesB:Person {name:'James L. Brooks', born:1940}) 142 | CREATE 143 | (JackN)-[:ACTED_IN {roles:['Melvin Udall']}]->(AsGoodAsItGets), 144 | (HelenH)-[:ACTED_IN {roles:['Carol Connelly']}]->(AsGoodAsItGets), 145 | (GregK)-[:ACTED_IN {roles:['Simon Bishop']}]->(AsGoodAsItGets), 146 | (CubaG)-[:ACTED_IN {roles:['Frank Sachs']}]->(AsGoodAsItGets), 147 | (JamesB)-[:DIRECTED]->(AsGoodAsItGets) 148 | 149 | CREATE (WhatDreamsMayCome:Movie {title:'What Dreams May Come', released:1998, tagline:'After life there is more. The end is just the beginning.'}) 150 | CREATE (AnnabellaS:Person {name:'Annabella Sciorra', born:1960}) 151 | CREATE (MaxS:Person {name:'Max von Sydow', born:1929}) 152 | CREATE (WernerH:Person {name:'Werner Herzog', born:1942}) 153 | CREATE (Robin:Person {name:'Robin Williams', born:1951}) 154 | CREATE (VincentW:Person {name:'Vincent Ward', born:1956}) 155 | CREATE 156 | (Robin)-[:ACTED_IN {roles:['Chris Nielsen']}]->(WhatDreamsMayCome), 157 | (CubaG)-[:ACTED_IN {roles:['Albert Lewis']}]->(WhatDreamsMayCome), 158 | (AnnabellaS)-[:ACTED_IN {roles:['Annie Collins-Nielsen']}]->(WhatDreamsMayCome), 159 | (MaxS)-[:ACTED_IN {roles:['The Tracker']}]->(WhatDreamsMayCome), 160 | (WernerH)-[:ACTED_IN {roles:['The Face']}]->(WhatDreamsMayCome), 161 | (VincentW)-[:DIRECTED]->(WhatDreamsMayCome) 162 | 163 | CREATE (SnowFallingonCedars:Movie {title:'Snow Falling on Cedars', released:1999, tagline:'First loves last. Forever.'}) 164 | CREATE (EthanH:Person {name:'Ethan Hawke', born:1970}) 165 | CREATE (RickY:Person {name:'Rick Yune', born:1971}) 166 | CREATE (JamesC:Person {name:'James Cromwell', born:1940}) 167 | CREATE (ScottH:Person {name:'Scott Hicks', born:1953}) 168 | CREATE 169 | (EthanH)-[:ACTED_IN {roles:['Ishmael Chambers']}]->(SnowFallingonCedars), 170 | (RickY)-[:ACTED_IN {roles:['Kazuo Miyamoto']}]->(SnowFallingonCedars), 171 | (MaxS)-[:ACTED_IN {roles:['Nels Gudmundsson']}]->(SnowFallingonCedars), 172 | (JamesC)-[:ACTED_IN {roles:['Judge Fielding']}]->(SnowFallingonCedars), 173 | (ScottH)-[:DIRECTED]->(SnowFallingonCedars) 174 | 175 | CREATE (YouveGotMail:Movie {title:"You've Got Mail", released:1998, tagline:'At odds in life... in love on-line.'}) 176 | CREATE (ParkerP:Person {name:'Parker Posey', born:1968}) 177 | CREATE (DaveC:Person {name:'Dave Chappelle', born:1973}) 178 | CREATE (SteveZ:Person {name:'Steve Zahn', born:1967}) 179 | CREATE (TomH:Person {name:'Tom Hanks', born:1956}) 180 | CREATE (NoraE:Person {name:'Nora Ephron', born:1941}) 181 | CREATE 182 | (TomH)-[:ACTED_IN {roles:['Joe Fox']}]->(YouveGotMail), 183 | (MegR)-[:ACTED_IN {roles:['Kathleen Kelly']}]->(YouveGotMail), 184 | (GregK)-[:ACTED_IN {roles:['Frank Navasky']}]->(YouveGotMail), 185 | (ParkerP)-[:ACTED_IN {roles:['Patricia Eden']}]->(YouveGotMail), 186 | (DaveC)-[:ACTED_IN {roles:['Kevin Jackson']}]->(YouveGotMail), 187 | (SteveZ)-[:ACTED_IN {roles:['George Pappas']}]->(YouveGotMail), 188 | (NoraE)-[:DIRECTED]->(YouveGotMail) 189 | 190 | CREATE (SleeplessInSeattle:Movie {title:'Sleepless in Seattle', released:1993, tagline:'What if someone you never met, someone you never saw, someone you never knew was the only someone for you?'}) 191 | CREATE (RitaW:Person {name:'Rita Wilson', born:1956}) 192 | CREATE (BillPull:Person {name:'Bill Pullman', born:1953}) 193 | CREATE (VictorG:Person {name:'Victor Garber', born:1949}) 194 | CREATE (RosieO:Person {name:"Rosie O'Donnell", born:1962}) 195 | CREATE 196 | (TomH)-[:ACTED_IN {roles:['Sam Baldwin']}]->(SleeplessInSeattle), 197 | (MegR)-[:ACTED_IN {roles:['Annie Reed']}]->(SleeplessInSeattle), 198 | (RitaW)-[:ACTED_IN {roles:['Suzy']}]->(SleeplessInSeattle), 199 | (BillPull)-[:ACTED_IN {roles:['Walter']}]->(SleeplessInSeattle), 200 | (VictorG)-[:ACTED_IN {roles:['Greg']}]->(SleeplessInSeattle), 201 | (RosieO)-[:ACTED_IN {roles:['Becky']}]->(SleeplessInSeattle), 202 | (NoraE)-[:DIRECTED]->(SleeplessInSeattle) 203 | 204 | CREATE (JoeVersustheVolcano:Movie {title:'Joe Versus the Volcano', released:1990, tagline:'A story of love, lava and burning desire.'}) 205 | CREATE (JohnS:Person {name:'John Patrick Stanley', born:1950}) 206 | CREATE (Nathan:Person {name:'Nathan Lane', born:1956}) 207 | CREATE 208 | (TomH)-[:ACTED_IN {roles:['Joe Banks']}]->(JoeVersustheVolcano), 209 | (MegR)-[:ACTED_IN {roles:['DeDe', 'Angelica Graynamore', 'Patricia Graynamore']}]->(JoeVersustheVolcano), 210 | (Nathan)-[:ACTED_IN {roles:['Baw']}]->(JoeVersustheVolcano), 211 | (JohnS)-[:DIRECTED]->(JoeVersustheVolcano) 212 | 213 | CREATE (WhenHarryMetSally:Movie {title:'When Harry Met Sally', released:1998, tagline:'Can two friends sleep together and still love each other in the morning?'}) 214 | CREATE (BillyC:Person {name:'Billy Crystal', born:1948}) 215 | CREATE (CarrieF:Person {name:'Carrie Fisher', born:1956}) 216 | CREATE (BrunoK:Person {name:'Bruno Kirby', born:1949}) 217 | CREATE 218 | (BillyC)-[:ACTED_IN {roles:['Harry Burns']}]->(WhenHarryMetSally), 219 | (MegR)-[:ACTED_IN {roles:['Sally Albright']}]->(WhenHarryMetSally), 220 | (CarrieF)-[:ACTED_IN {roles:['Marie']}]->(WhenHarryMetSally), 221 | (BrunoK)-[:ACTED_IN {roles:['Jess']}]->(WhenHarryMetSally), 222 | (RobR)-[:DIRECTED]->(WhenHarryMetSally), 223 | (RobR)-[:PRODUCED]->(WhenHarryMetSally), 224 | (NoraE)-[:PRODUCED]->(WhenHarryMetSally), 225 | (NoraE)-[:WROTE]->(WhenHarryMetSally) 226 | 227 | CREATE (ThatThingYouDo:Movie {title:'That Thing You Do', released:1996, tagline:'In every life there comes a time when that thing you dream becomes that thing you do'}) 228 | CREATE (LivT:Person {name:'Liv Tyler', born:1977}) 229 | CREATE 230 | (TomH)-[:ACTED_IN {roles:['Mr. White']}]->(ThatThingYouDo), 231 | (LivT)-[:ACTED_IN {roles:['Faye Dolan']}]->(ThatThingYouDo), 232 | (Charlize)-[:ACTED_IN {roles:['Tina']}]->(ThatThingYouDo), 233 | (TomH)-[:DIRECTED]->(ThatThingYouDo) 234 | 235 | CREATE (TheReplacements:Movie {title:'The Replacements', released:2000, tagline:'Pain heals, Chicks dig scars... Glory lasts forever'}) 236 | CREATE (Brooke:Person {name:'Brooke Langton', born:1970}) 237 | CREATE (Gene:Person {name:'Gene Hackman', born:1930}) 238 | CREATE (Orlando:Person {name:'Orlando Jones', born:1968}) 239 | CREATE (Howard:Person {name:'Howard Deutch', born:1950}) 240 | CREATE 241 | (Keanu)-[:ACTED_IN {roles:['Shane Falco']}]->(TheReplacements), 242 | (Brooke)-[:ACTED_IN {roles:['Annabelle Farrell']}]->(TheReplacements), 243 | (Gene)-[:ACTED_IN {roles:['Jimmy McGinty']}]->(TheReplacements), 244 | (Orlando)-[:ACTED_IN {roles:['Clifford Franklin']}]->(TheReplacements), 245 | (Howard)-[:DIRECTED]->(TheReplacements) 246 | 247 | CREATE (RescueDawn:Movie {title:'RescueDawn', released:2006, tagline:"Based on the extraordinary true story of one man's fight for freedom"}) 248 | CREATE (ChristianB:Person {name:'Christian Bale', born:1974}) 249 | CREATE (ZachG:Person {name:'Zach Grenier', born:1954}) 250 | CREATE 251 | (MarshallB)-[:ACTED_IN {roles:['Admiral']}]->(RescueDawn), 252 | (ChristianB)-[:ACTED_IN {roles:['Dieter Dengler']}]->(RescueDawn), 253 | (ZachG)-[:ACTED_IN {roles:['Squad Leader']}]->(RescueDawn), 254 | (SteveZ)-[:ACTED_IN {roles:['Duane']}]->(RescueDawn), 255 | (WernerH)-[:DIRECTED]->(RescueDawn) 256 | 257 | CREATE (TheBirdcage:Movie {title:'The Birdcage', released:1996, tagline:'Come as you are'}) 258 | CREATE (MikeN:Person {name:'Mike Nichols', born:1931}) 259 | CREATE 260 | (Robin)-[:ACTED_IN {roles:['Armand Goldman']}]->(TheBirdcage), 261 | (Nathan)-[:ACTED_IN {roles:['Albert Goldman']}]->(TheBirdcage), 262 | (Gene)-[:ACTED_IN {roles:['Sen. Kevin Keeley']}]->(TheBirdcage), 263 | (MikeN)-[:DIRECTED]->(TheBirdcage) 264 | 265 | CREATE (Unforgiven:Movie {title:'Unforgiven', released:1992, tagline:"It's a hell of a thing, killing a man"}) 266 | CREATE (RichardH:Person {name:'Richard Harris', born:1930}) 267 | CREATE (ClintE:Person {name:'Clint Eastwood', born:1930}) 268 | CREATE 269 | (RichardH)-[:ACTED_IN {roles:['English Bob']}]->(Unforgiven), 270 | (ClintE)-[:ACTED_IN {roles:['Bill Munny']}]->(Unforgiven), 271 | (Gene)-[:ACTED_IN {roles:['Little Bill Daggett']}]->(Unforgiven), 272 | (ClintE)-[:DIRECTED]->(Unforgiven) 273 | 274 | CREATE (JohnnyMnemonic:Movie {title:'Johnny Mnemonic', released:1995, tagline:'The hottest data on earth. In the coolest head in town'}) 275 | CREATE (Takeshi:Person {name:'Takeshi Kitano', born:1947}) 276 | CREATE (Dina:Person {name:'Dina Meyer', born:1968}) 277 | CREATE (IceT:Person {name:'Ice-T', born:1958}) 278 | CREATE (RobertL:Person {name:'Robert Longo', born:1953}) 279 | CREATE 280 | (Keanu)-[:ACTED_IN {roles:['Johnny Mnemonic']}]->(JohnnyMnemonic), 281 | (Takeshi)-[:ACTED_IN {roles:['Takahashi']}]->(JohnnyMnemonic), 282 | (Dina)-[:ACTED_IN {roles:['Jane']}]->(JohnnyMnemonic), 283 | (IceT)-[:ACTED_IN {roles:['J-Bone']}]->(JohnnyMnemonic), 284 | (RobertL)-[:DIRECTED]->(JohnnyMnemonic) 285 | 286 | CREATE (CloudAtlas:Movie {title:'Cloud Atlas', released:2012, tagline:'Everything is connected'}) 287 | CREATE (HalleB:Person {name:'Halle Berry', born:1966}) 288 | CREATE (JimB:Person {name:'Jim Broadbent', born:1949}) 289 | CREATE (TomT:Person {name:'Tom Tykwer', born:1965}) 290 | CREATE (DavidMitchell:Person {name:'David Mitchell', born:1969}) 291 | CREATE (StefanArndt:Person {name:'Stefan Arndt', born:1961}) 292 | CREATE 293 | (TomH)-[:ACTED_IN {roles:['Zachry', 'Dr. Henry Goose', 'Isaac Sachs', 'Dermot Hoggins']}]->(CloudAtlas), 294 | (Hugo)-[:ACTED_IN {roles:['Bill Smoke', 'Haskell Moore', 'Tadeusz Kesselring', 'Nurse Noakes', 'Boardman Mephi', 'Old Georgie']}]->(CloudAtlas), 295 | (HalleB)-[:ACTED_IN {roles:['Luisa Rey', 'Jocasta Ayrs', 'Ovid', 'Meronym']}]->(CloudAtlas), 296 | (JimB)-[:ACTED_IN {roles:['Vyvyan Ayrs', 'Captain Molyneux', 'Timothy Cavendish']}]->(CloudAtlas), 297 | (TomT)-[:DIRECTED]->(CloudAtlas), 298 | (LillyW)-[:DIRECTED]->(CloudAtlas), 299 | (LanaW)-[:DIRECTED]->(CloudAtlas), 300 | (DavidMitchell)-[:WROTE]->(CloudAtlas), 301 | (StefanArndt)-[:PRODUCED]->(CloudAtlas) 302 | 303 | CREATE (TheDaVinciCode:Movie {title:'The Da Vinci Code', released:2006, tagline:'Break The Codes'}) 304 | CREATE (IanM:Person {name:'Ian McKellen', born:1939}) 305 | CREATE (AudreyT:Person {name:'Audrey Tautou', born:1976}) 306 | CREATE (PaulB:Person {name:'Paul Bettany', born:1971}) 307 | CREATE (RonH:Person {name:'Ron Howard', born:1954}) 308 | CREATE 309 | (TomH)-[:ACTED_IN {roles:['Dr. Robert Langdon']}]->(TheDaVinciCode), 310 | (IanM)-[:ACTED_IN {roles:['Sir Leight Teabing']}]->(TheDaVinciCode), 311 | (AudreyT)-[:ACTED_IN {roles:['Sophie Neveu']}]->(TheDaVinciCode), 312 | (PaulB)-[:ACTED_IN {roles:['Silas']}]->(TheDaVinciCode), 313 | (RonH)-[:DIRECTED]->(TheDaVinciCode) 314 | 315 | CREATE (VforVendetta:Movie {title:'V for Vendetta', released:2006, tagline:'Freedom! Forever!'}) 316 | CREATE (NatalieP:Person {name:'Natalie Portman', born:1981}) 317 | CREATE (StephenR:Person {name:'Stephen Rea', born:1946}) 318 | CREATE (JohnH:Person {name:'John Hurt', born:1940}) 319 | CREATE (BenM:Person {name: 'Ben Miles', born:1967}) 320 | CREATE 321 | (Hugo)-[:ACTED_IN {roles:['V']}]->(VforVendetta), 322 | (NatalieP)-[:ACTED_IN {roles:['Evey Hammond']}]->(VforVendetta), 323 | (StephenR)-[:ACTED_IN {roles:['Eric Finch']}]->(VforVendetta), 324 | (JohnH)-[:ACTED_IN {roles:['High Chancellor Adam Sutler']}]->(VforVendetta), 325 | (BenM)-[:ACTED_IN {roles:['Dascomb']}]->(VforVendetta), 326 | (JamesM)-[:DIRECTED]->(VforVendetta), 327 | (LillyW)-[:PRODUCED]->(VforVendetta), 328 | (LanaW)-[:PRODUCED]->(VforVendetta), 329 | (JoelS)-[:PRODUCED]->(VforVendetta), 330 | (LillyW)-[:WROTE]->(VforVendetta), 331 | (LanaW)-[:WROTE]->(VforVendetta) 332 | 333 | CREATE (SpeedRacer:Movie {title:'Speed Racer', released:2008, tagline:'Speed has no limits'}) 334 | CREATE (EmileH:Person {name:'Emile Hirsch', born:1985}) 335 | CREATE (JohnG:Person {name:'John Goodman', born:1960}) 336 | CREATE (SusanS:Person {name:'Susan Sarandon', born:1946}) 337 | CREATE (MatthewF:Person {name:'Matthew Fox', born:1966}) 338 | CREATE (ChristinaR:Person {name:'Christina Ricci', born:1980}) 339 | CREATE (Rain:Person {name:'Rain', born:1982}) 340 | CREATE 341 | (EmileH)-[:ACTED_IN {roles:['Speed Racer']}]->(SpeedRacer), 342 | (JohnG)-[:ACTED_IN {roles:['Pops']}]->(SpeedRacer), 343 | (SusanS)-[:ACTED_IN {roles:['Mom']}]->(SpeedRacer), 344 | (MatthewF)-[:ACTED_IN {roles:['Racer X']}]->(SpeedRacer), 345 | (ChristinaR)-[:ACTED_IN {roles:['Trixie']}]->(SpeedRacer), 346 | (Rain)-[:ACTED_IN {roles:['Taejo Togokahn']}]->(SpeedRacer), 347 | (BenM)-[:ACTED_IN {roles:['Cass Jones']}]->(SpeedRacer), 348 | (LillyW)-[:DIRECTED]->(SpeedRacer), 349 | (LanaW)-[:DIRECTED]->(SpeedRacer), 350 | (LillyW)-[:WROTE]->(SpeedRacer), 351 | (LanaW)-[:WROTE]->(SpeedRacer), 352 | (JoelS)-[:PRODUCED]->(SpeedRacer) 353 | 354 | CREATE (NinjaAssassin:Movie {title:'Ninja Assassin', released:2009, tagline:'Prepare to enter a secret world of assassins'}) 355 | CREATE (NaomieH:Person {name:'Naomie Harris'}) 356 | CREATE 357 | (Rain)-[:ACTED_IN {roles:['Raizo']}]->(NinjaAssassin), 358 | (NaomieH)-[:ACTED_IN {roles:['Mika Coretti']}]->(NinjaAssassin), 359 | (RickY)-[:ACTED_IN {roles:['Takeshi']}]->(NinjaAssassin), 360 | (BenM)-[:ACTED_IN {roles:['Ryan Maslow']}]->(NinjaAssassin), 361 | (JamesM)-[:DIRECTED]->(NinjaAssassin), 362 | (LillyW)-[:PRODUCED]->(NinjaAssassin), 363 | (LanaW)-[:PRODUCED]->(NinjaAssassin), 364 | (JoelS)-[:PRODUCED]->(NinjaAssassin) 365 | 366 | CREATE (TheGreenMile:Movie {title:'The Green Mile', released:1999, tagline:"Walk a mile you'll never forget."}) 367 | CREATE (MichaelD:Person {name:'Michael Clarke Duncan', born:1957}) 368 | CREATE (DavidM:Person {name:'David Morse', born:1953}) 369 | CREATE (SamR:Person {name:'Sam Rockwell', born:1968}) 370 | CREATE (GaryS:Person {name:'Gary Sinise', born:1955}) 371 | CREATE (PatriciaC:Person {name:'Patricia Clarkson', born:1959}) 372 | CREATE (FrankD:Person {name:'Frank Darabont', born:1959}) 373 | CREATE 374 | (TomH)-[:ACTED_IN {roles:['Paul Edgecomb']}]->(TheGreenMile), 375 | (MichaelD)-[:ACTED_IN {roles:['John Coffey']}]->(TheGreenMile), 376 | (DavidM)-[:ACTED_IN {roles:['Brutus "Brutal" Howell']}]->(TheGreenMile), 377 | (BonnieH)-[:ACTED_IN {roles:['Jan Edgecomb']}]->(TheGreenMile), 378 | (JamesC)-[:ACTED_IN {roles:['Warden Hal Moores']}]->(TheGreenMile), 379 | (SamR)-[:ACTED_IN {roles:['"Wild Bill" Wharton']}]->(TheGreenMile), 380 | (GaryS)-[:ACTED_IN {roles:['Burt Hammersmith']}]->(TheGreenMile), 381 | (PatriciaC)-[:ACTED_IN {roles:['Melinda Moores']}]->(TheGreenMile), 382 | (FrankD)-[:DIRECTED]->(TheGreenMile) 383 | 384 | CREATE (FrostNixon:Movie {title:'Frost/Nixon', released:2008, tagline:'400 million people were waiting for the truth.'}) 385 | CREATE (FrankL:Person {name:'Frank Langella', born:1938}) 386 | CREATE (MichaelS:Person {name:'Michael Sheen', born:1969}) 387 | CREATE (OliverP:Person {name:'Oliver Platt', born:1960}) 388 | CREATE 389 | (FrankL)-[:ACTED_IN {roles:['Richard Nixon']}]->(FrostNixon), 390 | (MichaelS)-[:ACTED_IN {roles:['David Frost']}]->(FrostNixon), 391 | (KevinB)-[:ACTED_IN {roles:['Jack Brennan']}]->(FrostNixon), 392 | (OliverP)-[:ACTED_IN {roles:['Bob Zelnick']}]->(FrostNixon), 393 | (SamR)-[:ACTED_IN {roles:['James Reston, Jr.']}]->(FrostNixon), 394 | (RonH)-[:DIRECTED]->(FrostNixon) 395 | 396 | CREATE (Hoffa:Movie {title:'Hoffa', released:1992, tagline:"He didn't want law. He wanted justice."}) 397 | CREATE (DannyD:Person {name:'Danny DeVito', born:1944}) 398 | CREATE (JohnR:Person {name:'John C. Reilly', born:1965}) 399 | CREATE 400 | (JackN)-[:ACTED_IN {roles:['Hoffa']}]->(Hoffa), 401 | (DannyD)-[:ACTED_IN {roles:['Robert "Bobby" Ciaro']}]->(Hoffa), 402 | (JTW)-[:ACTED_IN {roles:['Frank Fitzsimmons']}]->(Hoffa), 403 | (JohnR)-[:ACTED_IN {roles:['Peter "Pete" Connelly']}]->(Hoffa), 404 | (DannyD)-[:DIRECTED]->(Hoffa) 405 | 406 | CREATE (Apollo13:Movie {title:'Apollo 13', released:1995, tagline:'Houston, we have a problem.'}) 407 | CREATE (EdH:Person {name:'Ed Harris', born:1950}) 408 | CREATE (BillPax:Person {name:'Bill Paxton', born:1955}) 409 | CREATE 410 | (TomH)-[:ACTED_IN {roles:['Jim Lovell']}]->(Apollo13), 411 | (KevinB)-[:ACTED_IN {roles:['Jack Swigert']}]->(Apollo13), 412 | (EdH)-[:ACTED_IN {roles:['Gene Kranz']}]->(Apollo13), 413 | (BillPax)-[:ACTED_IN {roles:['Fred Haise']}]->(Apollo13), 414 | (GaryS)-[:ACTED_IN {roles:['Ken Mattingly']}]->(Apollo13), 415 | (RonH)-[:DIRECTED]->(Apollo13) 416 | 417 | CREATE (Twister:Movie {title:'Twister', released:1996, tagline:"Don't Breathe. Don't Look Back."}) 418 | CREATE (PhilipH:Person {name:'Philip Seymour Hoffman', born:1967}) 419 | CREATE (JanB:Person {name:'Jan de Bont', born:1943}) 420 | CREATE 421 | (BillPax)-[:ACTED_IN {roles:['Bill Harding']}]->(Twister), 422 | (HelenH)-[:ACTED_IN {roles:['Dr. Jo Harding']}]->(Twister), 423 | (ZachG)-[:ACTED_IN {roles:['Eddie']}]->(Twister), 424 | (PhilipH)-[:ACTED_IN {roles:['Dustin "Dusty" Davis']}]->(Twister), 425 | (JanB)-[:DIRECTED]->(Twister) 426 | 427 | CREATE (CastAway:Movie {title:'Cast Away', released:2000, tagline:'At the edge of the world, his journey begins.'}) 428 | CREATE (RobertZ:Person {name:'Robert Zemeckis', born:1951}) 429 | CREATE 430 | (TomH)-[:ACTED_IN {roles:['Chuck Noland']}]->(CastAway), 431 | (HelenH)-[:ACTED_IN {roles:['Kelly Frears']}]->(CastAway), 432 | (RobertZ)-[:DIRECTED]->(CastAway) 433 | 434 | CREATE (OneFlewOvertheCuckoosNest:Movie {title:"One Flew Over the Cuckoo's Nest", released:1975, tagline:"If he's crazy, what does that make you?"}) 435 | CREATE (MilosF:Person {name:'Milos Forman', born:1932}) 436 | CREATE 437 | (JackN)-[:ACTED_IN {roles:['Randle McMurphy']}]->(OneFlewOvertheCuckoosNest), 438 | (DannyD)-[:ACTED_IN {roles:['Martini']}]->(OneFlewOvertheCuckoosNest), 439 | (MilosF)-[:DIRECTED]->(OneFlewOvertheCuckoosNest) 440 | 441 | CREATE (SomethingsGottaGive:Movie {title:"Something's Gotta Give", released:2003}) 442 | CREATE (DianeK:Person {name:'Diane Keaton', born:1946}) 443 | CREATE (NancyM:Person {name:'Nancy Meyers', born:1949}) 444 | CREATE 445 | (JackN)-[:ACTED_IN {roles:['Harry Sanborn']}]->(SomethingsGottaGive), 446 | (DianeK)-[:ACTED_IN {roles:['Erica Barry']}]->(SomethingsGottaGive), 447 | (Keanu)-[:ACTED_IN {roles:['Julian Mercer']}]->(SomethingsGottaGive), 448 | (NancyM)-[:DIRECTED]->(SomethingsGottaGive), 449 | (NancyM)-[:PRODUCED]->(SomethingsGottaGive), 450 | (NancyM)-[:WROTE]->(SomethingsGottaGive) 451 | 452 | CREATE (BicentennialMan:Movie {title:'Bicentennial Man', released:1999, tagline:"One robot's 200 year journey to become an ordinary man."}) 453 | CREATE (ChrisC:Person {name:'Chris Columbus', born:1958}) 454 | CREATE 455 | (Robin)-[:ACTED_IN {roles:['Andrew Marin']}]->(BicentennialMan), 456 | (OliverP)-[:ACTED_IN {roles:['Rupert Burns']}]->(BicentennialMan), 457 | (ChrisC)-[:DIRECTED]->(BicentennialMan) 458 | 459 | CREATE (CharlieWilsonsWar:Movie {title:"Charlie Wilson's War", released:2007, tagline:"A stiff drink. A little mascara. A lot of nerve. Who said they couldn't bring down the Soviet empire."}) 460 | CREATE (JuliaR:Person {name:'Julia Roberts', born:1967}) 461 | CREATE 462 | (TomH)-[:ACTED_IN {roles:['Rep. Charlie Wilson']}]->(CharlieWilsonsWar), 463 | (JuliaR)-[:ACTED_IN {roles:['Joanne Herring']}]->(CharlieWilsonsWar), 464 | (PhilipH)-[:ACTED_IN {roles:['Gust Avrakotos']}]->(CharlieWilsonsWar), 465 | (MikeN)-[:DIRECTED]->(CharlieWilsonsWar) 466 | 467 | CREATE (ThePolarExpress:Movie {title:'The Polar Express', released:2004, tagline:'This Holiday Season… Believe'}) 468 | CREATE 469 | (TomH)-[:ACTED_IN {roles:['Hero Boy', 'Father', 'Conductor', 'Hobo', 'Scrooge', 'Santa Claus']}]->(ThePolarExpress), 470 | (RobertZ)-[:DIRECTED]->(ThePolarExpress) 471 | 472 | CREATE (ALeagueofTheirOwn:Movie {title:'A League of Their Own', released:1992, tagline:'Once in a lifetime you get a chance to do something different.'}) 473 | CREATE (Madonna:Person {name:'Madonna', born:1954}) 474 | CREATE (GeenaD:Person {name:'Geena Davis', born:1956}) 475 | CREATE (LoriP:Person {name:'Lori Petty', born:1963}) 476 | CREATE (PennyM:Person {name:'Penny Marshall', born:1943}) 477 | CREATE 478 | (TomH)-[:ACTED_IN {roles:['Jimmy Dugan']}]->(ALeagueofTheirOwn), 479 | (GeenaD)-[:ACTED_IN {roles:['Dottie Hinson']}]->(ALeagueofTheirOwn), 480 | (LoriP)-[:ACTED_IN {roles:['Kit Keller']}]->(ALeagueofTheirOwn), 481 | (RosieO)-[:ACTED_IN {roles:['Doris Murphy']}]->(ALeagueofTheirOwn), 482 | (Madonna)-[:ACTED_IN {roles:['"All the Way" Mae Mordabito']}]->(ALeagueofTheirOwn), 483 | (BillPax)-[:ACTED_IN {roles:['Bob Hinson']}]->(ALeagueofTheirOwn), 484 | (PennyM)-[:DIRECTED]->(ALeagueofTheirOwn) 485 | 486 | CREATE (PaulBlythe:Person {name:'Paul Blythe'}) 487 | CREATE (AngelaScope:Person {name:'Angela Scope'}) 488 | CREATE (JessicaThompson:Person {name:'Jessica Thompson'}) 489 | CREATE (JamesThompson:Person {name:'James Thompson'}) 490 | 491 | CREATE 492 | (JamesThompson)-[:FOLLOWS]->(JessicaThompson), 493 | (AngelaScope)-[:FOLLOWS]->(JessicaThompson), 494 | (PaulBlythe)-[:FOLLOWS]->(AngelaScope) 495 | 496 | CREATE 497 | (JessicaThompson)-[:REVIEWED {summary:'An amazing journey', rating:95}]->(CloudAtlas), 498 | (JessicaThompson)-[:REVIEWED {summary:'Silly, but fun', rating:65}]->(TheReplacements), 499 | (JamesThompson)-[:REVIEWED {summary:'The coolest football movie ever', rating:100}]->(TheReplacements), 500 | (AngelaScope)-[:REVIEWED {summary:'Pretty funny at times', rating:62}]->(TheReplacements), 501 | (JessicaThompson)-[:REVIEWED {summary:'Dark, but compelling', rating:85}]->(Unforgiven), 502 | (JessicaThompson)-[:REVIEWED {summary:"Slapstick redeemed only by the Robin Williams and Gene Hackman's stellar performances", rating:45}]->(TheBirdcage), 503 | (JessicaThompson)-[:REVIEWED {summary:'A solid romp', rating:68}]->(TheDaVinciCode), 504 | (JamesThompson)-[:REVIEWED {summary:'Fun, but a little far fetched', rating:65}]->(TheDaVinciCode), 505 | (JessicaThompson)-[:REVIEWED {summary:'You had me at Jerry', rating:92}]->(JerryMaguire); --------------------------------------------------------------------------------