├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CONTRIBUTING.adoc ├── LICENSE.txt ├── README.adoc ├── etc ├── checkstyle │ ├── config.xml │ └── suppressions.xml ├── jreleaser │ └── changelog.md.tpl └── license.tpl ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main ├── assembly │ ├── README │ └── assembly-java.xml ├── java │ ├── module-info.java │ └── org │ │ └── neo4j │ │ └── sql2cypher │ │ ├── ManifestVersionProvider.java │ │ ├── Translator.java │ │ ├── TranslatorCLI.java │ │ ├── TranslatorConfig.java │ │ └── package-info.java └── resources │ └── META-INF │ └── native-image │ └── org.example │ └── jOOQ-cypher │ ├── native-image.properties │ ├── reflection-config.json │ └── resources-config.json └── test ├── java └── org │ └── neo4j │ └── sql2cypher │ └── TranslatorTest.java └── resources ├── dml.adoc ├── expressions.adoc ├── joins.adoc ├── predicates.adoc └── simple.adoc /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'README.adoc' 9 | pull_request: 10 | paths-ignore: 11 | - 'README.adoc' 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Set up JDK' 18 | uses: actions/setup-java@v3 19 | with: 20 | distribution: zulu 21 | java-version: 17 22 | 23 | - name: 'Cache Maven packages' 24 | uses: actions/cache@v3 25 | with: 26 | path: ~/.m2 27 | key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}-${{ github.sha }} 28 | 29 | - name: 'Checkout jOOQ' 30 | uses: actions/checkout@v3 31 | with: 32 | repository: jOOQ/jOOQ 33 | ref: stable-sql2cypher 34 | path: jOOQ 35 | 36 | - name: 'Install jOOQ Snapshot' 37 | working-directory: jOOQ 38 | run: mvn --no-transfer-progress -DskipTests -pl jOOQ -pl jOOQ-meta -am install 39 | 40 | - name: 'Checkout' 41 | uses: actions/checkout@v3 42 | with: 43 | path: sql2cypher 44 | 45 | - name: 'Clean and verify' 46 | working-directory: sql2cypher 47 | run: mvn --no-transfer-progress clean verify 48 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | create: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | release: 11 | if: (github.event_name != 'create' || github.event.ref_type == 'tag') 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: 'Set up JDK' 15 | uses: actions/setup-java@v3 16 | with: 17 | distribution: zulu 18 | java-version: 17 19 | 20 | - name: 'Prepare git' 21 | run: git config --global core.autocrlf false 22 | 23 | - name: 'Prepare branch name' 24 | if: (github.event_name == 'create' && github.event.ref_type == 'tag') 25 | run: > 26 | echo "refName=${GITHUB_REF##*/}" >> $GITHUB_ENV 27 | 28 | - name: 'Checkout jOOQ' 29 | uses: actions/checkout@v3 30 | with: 31 | repository: jOOQ/jOOQ 32 | ref: stable-sql2cypher 33 | path: jOOQ 34 | 35 | - name: 'Install jOOQ Snapshot' 36 | working-directory: jOOQ 37 | run: mvn --no-transfer-progress -DskipTests -pl jOOQ -pl jOOQ-meta -am install 38 | 39 | - name: 'Checkout relevant branch' 40 | uses: actions/checkout@v3 41 | with: 42 | path: sql2cypher 43 | ref: ${{ env.refName }} 44 | 45 | - name: 'Create jar' 46 | working-directory: sql2cypher 47 | run: ./mvnw --no-transfer-progress -Dfast clean package 48 | 49 | - name: 'Setup all required JDKs' 50 | working-directory: sql2cypher 51 | run: ./mvnw --no-transfer-progress jdks:setup-disco 52 | 53 | - name: 'Assemble jlink images' 54 | working-directory: sql2cypher 55 | run: ./mvnw --no-transfer-progress jreleaser:assemble 56 | 57 | - name: 'Create release' 58 | working-directory: sql2cypher 59 | env: 60 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | run: ./mvnw --no-transfer-progress jreleaser:full-release 62 | -------------------------------------------------------------------------------- /.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-contrib/sql2cypher/4602d047894deadc16c5564e4e063d9da7ec535e/.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.9.3/apache-maven-3.9.3-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 | -------------------------------------------------------------------------------- /CONTRIBUTING.adoc: -------------------------------------------------------------------------------- 1 | = Contributing 2 | 3 | == Building and compiling sql2cypher 4 | 5 | JDK 17 and Maven is required. Please run to test and package the project: 6 | 7 | WARNING: For the time being, you need to install a snapshot version of both https://github.com/jOOQ/jOOQ[jOOQ] and https://github.com/neo4j-contrib/cypher-dsl[cypher-dsl]. 8 | 9 | [source,bash] 10 | ---- 11 | git clone --depth 1 --branch stable-sql2cypher git@github.com:jOOQ/jOOQ.git 12 | cd jOOQ 13 | mvn -DskipTests -pl jOOQ -pl jOOQ-meta -am install 14 | cd - 15 | ---- 16 | 17 | [source,bash] 18 | ---- 19 | ./mvnw verify 20 | ---- 21 | 22 | There's a `fast` profile that will skip all the tests and validations: 23 | 24 | [source,bash] 25 | ---- 26 | ./mvnw -Dfast package 27 | ---- 28 | 29 | The build will create an assembly inside the `target` directory, with two executable scripts inside the `bin` sub-directory: 30 | 31 | [source,console] 32 | ---- 33 | ➜ sql2cypher git:(main) ✗ tree target/assembly 34 | target/assembly 35 | ├── bin 36 | │   ├── sql2cypher 37 | │   └── sql2cypher.bat 38 | └── lib 39 | ├── apiguardian-api-1.1.2.jar 40 | ├── jooq-3.18.0-SNAPSHOT.jar 41 | ├── neo4j-cypher-dsl-2023.0.1.jar 42 | ├── picocli-4.7.0.jar 43 | ├── r2dbc-spi-1.0.0.RELEASE.jar 44 | ├── reactive-streams-1.0.3.jar 45 | └── sql2cypher-1.0-SNAPSHOT.jar 46 | 47 | 2 directories, 9 files 48 | ---- 49 | 50 | === Native image (GraalVM) support 51 | 52 | There's somewhat support to run this application as native image, but jOOQ itself is not yet fully GraalVM compatible (see https://github.com/jOOQ/jOOQ/issues/8779[jOOQ 8779]). 53 | 54 | Please install GraalVM 22.3 for Java 17 and native image. One way is https://sdkman.io[SDKMAN!]: 55 | 56 | [source,bash] 57 | ---- 58 | sdk install java 22.3.r17-grl 59 | gu install native-image 60 | ---- 61 | 62 | Afterwards you can run our build for native image like this: 63 | 64 | [source,bash] 65 | ---- 66 | sdk use java 22.3.r17-grl 67 | ./mvnw -Dnative package 68 | file target/sql2cypher 69 | ---- 70 | 71 | Test the generated image like this: 72 | 73 | .Testing native image 74 | [source,bash] 75 | ---- 76 | time ./target/sql2cypher 'SELECT p.name, cast(p.age as STRING) 77 | FROM "Person" AS p 78 | WHERE p.age = 18 + rand()' 2>/dev/null 79 | ---- 80 | 81 | .Output and runtime native image 82 | [source,console] 83 | ---- 84 | MATCH (p:Person) 85 | WHERE p.age = (18 + rand()) 86 | RETURN p.name, toString(p.age) 87 | 88 | 0,01s user 0,01s system 73% cpu 0,025 total 89 | ---- 90 | 91 | The necessary `reflection-config.json` has been created partially via the GraalVM agent run like this 92 | 93 | [source,bash] 94 | ---- 95 | ./mvnw -Dfast package 96 | JAVA_OPTS=-agentlib:native-image-agent=config-output-dir=target/generated-native-image-config target/assembly/bin/sql2cypher "SELECT 1" 97 | ---- 98 | 99 | And a printing breakpoint in the constructor of `org.jooq.impl.ArrayDataType.ArrayDataType` with the following content: 100 | 101 | [source,java] 102 | ---- 103 | "{\"name\": \"" + elementType.getType().getName() + "[]\", \"allPublicConstructors\": true}," 104 | ---- 105 | 106 | == Tasks 107 | 108 | === Formatting sources / adding headers 109 | 110 | When you add new files, you can run 111 | 112 | [source,bash] 113 | ---- 114 | ./mvnw license:format 115 | ---- 116 | 117 | to add required headers automatically. 118 | 119 | We use https://github.com/spring-io/spring-javaformat[spring-javaformat] to format the source files. 120 | 121 | [source,bash] 122 | ---- 123 | ./mvnw spring-javaformat:apply 124 | ---- 125 | 126 | TIP: The Spring Developers write: "The source formatter does not fundamentally change your code. For example, it will not change the order of import statements. It is effectively limited to adding or removing whitespace and line feeds." 127 | This means the following checkstyle check might still fail. 128 | Some common errors: 129 | + 130 | Static imports, import `javax.*` and `java.*` before others 131 | + 132 | Static imports are helpful, yes, but when working with 2 builders in the same project (here jOOQ and Cypher-DSL), they can be quite confusing. 133 | 134 | There are plugins for https://github.com/spring-io/spring-javaformat#eclipse[Eclipse] and https://github.com/spring-io/spring-javaformat#intellij-idea[IntelliJ IDEA] and the Checkstyle settings https://github.com/spring-io/spring-javaformat#checkstyle-idea-plugin[can be imported as well]. 135 | We took those "as is" and just disabled the lambda check (requiring even single parameters to have parenthesis). 136 | 137 | === Sorting the build file 138 | 139 | To keep the dependencies in a predictable order, we enforce sorting of the build file. You can trigger this manually via 140 | 141 | [source,bash] 142 | ---- 143 | ./mvnw sortpom:verify@sort 144 | ---- 145 | -------------------------------------------------------------------------------- /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 | WARNING: This repository is achieved as of January 2024. We are very grateful towards https://www.datageekery.com and Lukas Eder for getting this up and running! 2 | The content of this experimental work now lives on as part of the official https://github.com/neo4j/neo4j-jdbc[Neo4j JDBC Driver], see the https://github.com/neo4j/neo4j-jdbc/tree/main/neo4j-jdbc-translator[translator module]. 3 | 4 | = Sql2Cypher Experimental Transpiler from SQL to Cypher 5 | 6 | This library is an experiment for converting SQL statements (first only read statements) to the appropriate Cypher equivalent. 7 | 8 | It is currently in a pre-alpha stage. 9 | 10 | == Example 11 | 12 | For example: 13 | 14 | [source,sql] 15 | ---- 16 | SELECT t.a, t.b 17 | FROM my_table AS t 18 | WHERE t.a = 1 19 | ---- 20 | 21 | can be translated into 22 | 23 | [source,cypher] 24 | ---- 25 | MATCH (T:`MY_TABLE`) 26 | WHERE T.A = 1 27 | RETURN T.A, T.B 28 | ---- 29 | 30 | More examples can be found in the link:src/test/resources[TCK]. 31 | 32 | * link:src/test/resources/simple.adoc[Simple Patterns] 33 | * link:src/test/resources/expressions.adoc[Expressions] 34 | * link:src/test/resources/predicates.adoc[Predicates] 35 | 36 | == Download 37 | 38 | Please go to https://github.com/neo4j-contrib/sql2cypher/releases/tag/early-access[the early access download] where we offer a bunch of binaries for various operating systems and architectures. 39 | These binaries come with a dedicated and prepackaged JVM, no need to install Java separately. 40 | 41 | Here's an example for macOS on Apple silicon, please adapt as necessary for your system and architecture: 42 | 43 | [source,bash] 44 | ---- 45 | curl -LO https://github.com/neo4j-contrib/sql2cypher/releases/download/early-access/sql2cypher-1.0-SNAPSHOT-osx-aarch_64.zip 46 | mkdir -p sql2cypher 47 | bsdtar xvf sql2cypher-1.0-SNAPSHOT-osx-aarch_64.zip --strip-components=1 -C sql2cypher 48 | ./sql2cypher/bin/sql2cypher -V 49 | ---- 50 | 51 | == Usage 52 | 53 | [source,console] 54 | ---- 55 | Usage: sql2cypher [-hV] [--disable-pretty-printing] 56 | [--parse-name-case=] 57 | [--sql-dialect=] 58 | [--table-to-label-mapping=]... [COMMAND] 59 | Translates SQL statements to Cypher queries. 60 | Any valid SQL statement that should be translated to Cypher 61 | --parse-name-case= 62 | How to parse names; valid values are: AS_IS, LOWER, 63 | LOWER_IF_UNQUOTED, UPPER, UPPER_IF_UNQUOTED, DEFAULT and 64 | the default is LOWER_IF_UNQUOTED 65 | --table-to-label-mapping= 66 | A table name that should be mapped to a specific label, 67 | repeat for multiple mappings 68 | --sql-dialect= 69 | The SQL dialect to use for parsing; valid values are: 70 | DEFAULT, CUBRID, DERBY, FIREBIRD, H2, HSQLDB, IGNITE, 71 | MARIADB, MYSQL, POSTGRES, SQLITE, YUGABYTEDB and the 72 | default is DEFAULT 73 | --disable-pretty-printing 74 | Disables pretty printing 75 | -h, --help Show this help message and exit. 76 | -V, --version Print version information and exit. 77 | Commands: 78 | help Display help information about the specified command. 79 | ---- 80 | 81 | == Build 82 | 83 | Please head over to our link:CONTRIBUTING.adoc[contributing instruction] to learn about build requirements, how to build this project and additional topics like native image with GraalVM. 84 | 85 | == Dependencies 86 | 87 | * https://github.com/jOOQ[jOOQ - SQL DSL] 88 | * https://github.com/neo4j-contrib/cypher-dsl[Neo4j Cypher-DSL] 89 | * https://github.com/remkop/picocli[PicoCli] 90 | -------------------------------------------------------------------------------- /etc/checkstyle/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /etc/checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /etc/jreleaser/changelog.md.tpl: -------------------------------------------------------------------------------- 1 | # What's Changed 2 | 3 | {{changelogChanges}} 4 | -------------------------------------------------------------------------------- /etc/license.tpl: -------------------------------------------------------------------------------- 1 | Copyright ${year} the original author or authors. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | https://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | 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 | 19 | 20 | 4.0.0 21 | 22 | org.neo4j 23 | sql2cypher 24 | 1.0-SNAPSHOT 25 | sql2cypher 26 | Experimental SQL to Cypher Transpiler using jooq and cypher-dsl 27 | 28 | 2023 29 | 30 | Neo4j, Neo4j Sweden AB 31 | https://neo4j.com 32 | 33 | 34 | 35 | The Apache Software License, Version 2.0 36 | https://www.apache.org/licenses/LICENSE-2.0.txt 37 | repo 38 | 39 | 40 | 41 | 42 | 43 | leder 44 | Lukas Eder 45 | lukas.eder at gmail.com 46 | jOOQ 47 | https://jooq.org 48 | 49 | Developer 50 | 51 | +1 52 | 53 | 54 | mhunger 55 | Michael Hunger 56 | michael.hunger at neo4j.com 57 | Neo Technology 58 | https://neo4j.com 59 | 60 | Developer 61 | 62 | +1 63 | 64 | 65 | msimons 66 | Michael Simons 67 | michael.simons at neo4j.com 68 | Neo Technology 69 | https://neo4j.com 70 | 71 | Developer 72 | 73 | +1 74 | 75 | 76 | 77 | 78 | 2.1.0 79 | ${project.build.directory} 80 | 2.5.7 81 | 3.24.1 82 | 10.6.0 83 | ${project.artifactId} 84 | 17 85 | zulu17.40.19-ca-jdk${jdkVersion} 86 | 17.0.6 87 | 3.19.0-SNAPSHOT 88 | 1.4.0 89 | 5.9.2 90 | 4.2.rc2 91 | org.neo4j.sql2cypher.TranslatorCLI 92 | 3.4.2 93 | 3.2.1 94 | 3.10.1 95 | 3.3.0 96 | 3.0.0-M7 97 | true 98 | ${java.version} 99 | 1.0.0.RC2 100 | 0.9.18 101 | 2023.5.0 102 | 4.7.0 103 | UTF-8 104 | UTF-8 105 | 1.0.0.RELEASE 106 | 1.0.3 107 | 3.2.0 108 | 0.0.35 109 | 110 | 111 | 112 | 113 | 114 | org.junit 115 | junit-bom 116 | ${junit-jupiter.version} 117 | pom 118 | import 119 | 120 | 121 | io.r2dbc 122 | r2dbc-spi 123 | ${r2dbc-spi.version} 124 | 125 | 126 | org.reactivestreams 127 | reactive-streams 128 | ${reactive-streams.version} 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | info.picocli 137 | picocli 138 | ${picocli.version} 139 | 140 | 143 | 144 | org.jooq 145 | jooq 146 | ${jooq.version} 147 | 148 | 149 | org.neo4j 150 | neo4j-cypher-dsl 151 | ${neo4j-cypher-dsl.version} 152 | 153 | 154 | 155 | org.asciidoctor 156 | asciidoctorj 157 | ${asciidoctorj.version} 158 | test 159 | 160 | 161 | 162 | org.assertj 163 | assertj-core 164 | ${assertj.version} 165 | test 166 | 167 | 168 | 169 | org.junit.jupiter 170 | junit-jupiter 171 | test 172 | 173 | 174 | org.neo4j 175 | neo4j-cypher-dsl-parser 176 | ${neo4j-cypher-dsl.version} 177 | test 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-checkstyle-plugin 187 | ${maven-checkstyle-plugin.version} 188 | 189 | **/module-info.java 190 | true 191 | etc/checkstyle/config.xml 192 | etc/checkstyle/suppressions.xml 193 | ${project.build.sourceEncoding} 194 | true 195 | true 196 | true 197 | 198 | 199 | 200 | com.puppycrawl.tools 201 | checkstyle 202 | ${checkstyle.version} 203 | 204 | 205 | io.spring.javaformat 206 | spring-javaformat-checkstyle 207 | ${spring-javaformat.version} 208 | 209 | 210 | 211 | 212 | maven-compiler-plugin 213 | ${maven-compiler-plugin.version} 214 | 215 | 216 | 217 | info.picocli 218 | picocli-codegen 219 | ${picocli.version} 220 | 221 | 222 | 223 | -Aproject=${project.groupId}/${project.artifactId} 224 | 225 | 226 | 227 | 228 | maven-surefire-plugin 229 | ${maven-surefire-plugin.version} 230 | 231 | 232 | 233 | 234 | 235 | 236 | com.github.ekryd.sortpom 237 | sortpom-maven-plugin 238 | ${sortpom-maven-plugin.version} 239 | 240 | ${project.build.sourceEncoding} 241 | true 242 | -1 243 | true 244 | scope,groupId,artifactId 245 | false 246 | false 247 | 248 | 249 | 250 | sort 251 | 252 | sort 253 | 254 | verify 255 | 256 | 257 | 258 | 259 | com.mycila 260 | license-maven-plugin 261 | ${license-maven-plugin.version} 262 | 263 |
etc/license.tpl
264 | true 265 | 266 | SCRIPT_STYLE 267 | 268 | 269 | ${project.inceptionYear} 270 | 271 | 272 | ** 273 | 274 | 275 | **/*.adoc 276 | **/*.sh 277 | **/*.tpl 278 | **/*.txt 279 | **/*.yaml 280 | **/*.extension 281 | **/README 282 | **/org.mockito.plugins.MockMaker 283 | 284 |
285 | 286 | 287 | validate 288 | 289 | check 290 | 291 | validate 292 | 293 | 294 |
295 | 296 | io.spring.javaformat 297 | spring-javaformat-maven-plugin 298 | ${spring-javaformat.version} 299 | 300 | 301 | 302 | validate 303 | 304 | validate 305 | true 306 | 307 | 308 | 309 | 310 | org.apache.maven.plugins 311 | maven-checkstyle-plugin 312 | 313 | 314 | checkstyle-validation 315 | 316 | check 317 | 318 | validate 319 | true 320 | 321 | 322 | 323 | 324 | org.apache.maven.plugins 325 | maven-jar-plugin 326 | ${maven-jar-plugin.version} 327 | 328 | 329 | 330 | ${mainClass} 331 | true 332 | true 333 | 334 | 335 | true 336 | 337 | 338 | 339 | org.codehaus.mojo 340 | appassembler-maven-plugin 341 | ${appassembler-maven-plugin.version} 342 | 343 | ${project.build.directory}/assembly 344 | flat 345 | lib 346 | 347 | 348 | ${mainClass} 349 | sql2cypher 350 | 351 | 352 | 353 | 354 | 355 | make-distribution 356 | 357 | assemble 358 | 359 | package 360 | 361 | 362 | 363 | 364 | org.apache.maven.plugins 365 | maven-assembly-plugin 366 | ${maven-assembly-plugin.version} 367 | 368 | UTF-8 369 | false 370 | 371 | 372 | 373 | assembly-java 374 | 375 | single 376 | 377 | package 378 | 379 | sql2cypher-${project.version} 380 | false 381 | 382 | src/main/assembly/assembly-java.xml 383 | 384 | 385 | 386 | 387 | 388 | 389 | org.apache.maven.plugins 390 | maven-dependency-plugin 391 | 3.5.0 392 | 393 | 394 | default 395 | 396 | copy-dependencies 397 | 398 | package 399 | 400 | compile 401 | ${project.build.directory}/modules 402 | 403 | 404 | 405 | 406 | 407 | org.moditect 408 | moditect-maven-plugin 409 | ${moditect-maven-plugin.version} 410 | 411 | 412 | add-module-infos 413 | 414 | add-module-info 415 | 416 | package 417 | 418 | ${project.build.directory}/modules 419 | true 420 | 421 | 422 | 423 | io.r2dbc 424 | r2dbc-spi 425 | ${r2dbc-spi.version} 426 | 427 | 432 | 433 | 434 | 435 | org.reactivestreams 436 | reactive-streams 437 | ${reactive-streams.version} 438 | 439 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | org.jreleaser 452 | jdks-maven-plugin 453 | 1.4.0 454 | 455 | 456 | 457 | linux-x86_64 458 | linux-x86_64 459 | ${jdkVersion} 460 | 461 | 462 | linux-aarch_64 463 | linux-aarch64 464 | ${jdkVersion} 465 | tar.gz 466 | 467 | 468 | osx-x86_64 469 | osx-x86_64 470 | ${jdkVersion} 471 | 472 | 473 | osx-aarch_64 474 | osx-aarch64 475 | ${jdkVersion} 476 | 477 | 478 | windows-x86_64 479 | windows-x86_64 480 | ${jdkVersion} 481 | 482 | 483 | 484 | 485 | 486 | org.jreleaser 487 | jreleaser-maven-plugin 488 | ${jreleaser-maven-plugin.version} 489 | 490 | 491 | 492 | ${project.artifactId} 493 | 494 | 495 | 496 | 497 | ALWAYS 498 | true 499 | CLI 500 | ${imageName}-{{projectVersion}} 501 | 502 | 503 | ${project.build.directory}/jdks/linux-x86_64/${jdkFilePrefix}-linux_x64 504 | linux-x86_64 505 | 506 | 507 | ${project.build.directory}/jdks/linux-aarch_64/${jdkFilePrefix}-linux_aarch64 508 | linux-aarch_64 509 | 510 | 511 | ${project.build.directory}/jdks/osx-x86_64/${jdkFilePrefix}-macosx_x64 512 | osx-x86_64 513 | 514 | 515 | ${project.build.directory}/jdks/osx-aarch_64/${jdkFilePrefix}-macosx_aarch64 516 | osx-aarch_64 517 | 518 | 519 | ${project.build.directory}/jdks/windows-x86_64/${jdkFilePrefix}-win_x64 520 | windows-x86_64 521 | 522 | 523 | 524 | true 525 | ${java.version} 526 | true 527 | 528 | 529 | ${project.build.directory}/${project.build.finalName}.jar 530 | 531 | 532 | 533 | ${project.build.directory}/modules/*.jar 534 | 535 | 536 | 537 | org.neo4j.sql2cypher 538 | ${mainClass} 539 | 540 | 541 | 542 | 543 | 544 | 545 | true 546 | {{projectVersion}} 547 | {{projectVersion}} 548 | main 549 | 550 | ALWAYS 551 | etc/jreleaser/changelog.md.tpl 552 | - {{commitShortHash}} {{commitTitle}} 553 | conventional-commits 554 | 555 | 556 | 557 | 558 | 559 | JAVA_BINARY 560 | 561 | 562 | {{artifactsDir}}/{{projectName}}-{{projectVersion}}.zip 563 | 564 | 565 | 566 | ${java.version} 567 | ${mainClass} 568 | 569 | 570 | 571 | 572 | 573 | 574 |
575 |
576 | 577 | 578 | 579 | fast 580 | 581 | 582 | fast 583 | 584 | 585 | 586 | true 587 | true 588 | true 589 | true 590 | true 591 | true 592 | true 593 | true 594 | true 595 | true 596 | true 597 | true 598 | true 599 | 600 | 601 | 602 | native 603 | 604 | 605 | native 606 | 607 | 608 | 609 | 610 | 611 | org.graalvm.buildtools 612 | native-maven-plugin 613 | ${native.maven.plugin.version} 614 | true 615 | 616 | 22.3 617 | ${mainClass} 618 | ${imageName} 619 | 620 | --no-fallback 621 | -H:+PrintClassInitialization 622 | 623 | 624 | 625 | 626 | create-native-image 627 | 628 | compile-no-fork 629 | 630 | package 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 |
639 | -------------------------------------------------------------------------------- /src/main/assembly/README: -------------------------------------------------------------------------------- 1 | This is [sql2cypher][1]. 2 | 3 | Use 4 | 5 | ./bin/sql2cypher -h 6 | 7 | on a Unix like machine for usage instructions and 8 | 9 | ./bin/sql2cypher.bat -h 10 | 11 | on Windows. 12 | 13 | [1]: https://github.com/neo4j-contrib/sql2cypher 14 | -------------------------------------------------------------------------------- /src/main/assembly/assembly-java.xml: -------------------------------------------------------------------------------- 1 | 2 | 19 | 23 | dist 24 | 25 | zip 26 | 27 | 28 | 29 | ${project.basedir}/LICENSE.txt 30 | LICENSE 31 | 32 | 33 | 34 | src/main/assembly/README 35 | 36 | 37 | 38 | 39 | 40 | ${project.build.directory}/assembly 41 | 42 | 43 | 44 | ${project.build.directory}/assembly/bin 45 | bin 46 | 744 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/main/java/module-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | module org.neo4j.sql2cypher { 17 | requires org.jooq; 18 | requires org.neo4j.cypherdsl.core; 19 | requires info.picocli; 20 | 21 | opens org.neo4j.sql2cypher to info.picocli; 22 | } -------------------------------------------------------------------------------- /src/main/java/org/neo4j/sql2cypher/ManifestVersionProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.neo4j.sql2cypher; 17 | 18 | import java.io.IOException; 19 | import java.util.jar.Attributes; 20 | import java.util.jar.Manifest; 21 | 22 | import picocli.CommandLine; 23 | import picocli.CommandLine.IVersionProvider; 24 | 25 | /** 26 | * Version provider modelled after the official example. here 28 | * 29 | * @author Michael J. Simons 30 | */ 31 | final class ManifestVersionProvider implements IVersionProvider { 32 | 33 | @Override 34 | public String[] getVersion() throws IOException { 35 | var resources = CommandLine.class.getClassLoader().getResources("META-INF/MANIFEST.MF"); 36 | while (resources.hasMoreElements()) { 37 | var url = resources.nextElement(); 38 | var manifest = new Manifest(url.openStream()); 39 | var attributes = manifest.getMainAttributes(); 40 | if ("sql2cypher".equals(get(attributes, "Implementation-Title"))) { 41 | return new String[] { get(attributes, "Implementation-Version").toString() }; 42 | } 43 | } 44 | return new String[0]; 45 | } 46 | 47 | private static Object get(Attributes attributes, String key) { 48 | return attributes.get(new Attributes.Name(key)); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/sql2cypher/Translator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.neo4j.sql2cypher; 17 | 18 | import java.util.Arrays; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Locale; 22 | import java.util.Objects; 23 | import java.util.Optional; 24 | import java.util.function.BiFunction; 25 | import java.util.function.Predicate; 26 | import java.util.function.Supplier; 27 | import java.util.stream.Collectors; 28 | 29 | import org.jooq.Asterisk; 30 | import org.jooq.CreateTableElementListStep; 31 | import org.jooq.DSLContext; 32 | import org.jooq.Field; 33 | import org.jooq.Param; 34 | import org.jooq.Parser; 35 | import org.jooq.QualifiedAsterisk; 36 | import org.jooq.Query; 37 | import org.jooq.QueryPart; 38 | import org.jooq.Row; 39 | import org.jooq.Select; 40 | import org.jooq.SelectField; 41 | import org.jooq.SelectFieldOrAsterisk; 42 | import org.jooq.SortField; 43 | import org.jooq.Table; 44 | import org.jooq.TableField; 45 | import org.jooq.conf.ParseWithMetaLookups; 46 | import org.jooq.impl.DSL; 47 | import org.jooq.impl.DefaultConfiguration; 48 | import org.jooq.impl.QOM; 49 | import org.jooq.impl.QOM.TableAlias; 50 | import org.neo4j.cypherdsl.core.Case; 51 | import org.neo4j.cypherdsl.core.Condition; 52 | import org.neo4j.cypherdsl.core.Cypher; 53 | import org.neo4j.cypherdsl.core.ExposesRelationships; 54 | import org.neo4j.cypherdsl.core.Expression; 55 | import org.neo4j.cypherdsl.core.Functions; 56 | import org.neo4j.cypherdsl.core.Node; 57 | import org.neo4j.cypherdsl.core.PatternElement; 58 | import org.neo4j.cypherdsl.core.Relationship; 59 | import org.neo4j.cypherdsl.core.RelationshipChain; 60 | import org.neo4j.cypherdsl.core.ResultStatement; 61 | import org.neo4j.cypherdsl.core.SortItem; 62 | import org.neo4j.cypherdsl.core.Statement; 63 | import org.neo4j.cypherdsl.core.StatementBuilder; 64 | import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingWithWhere; 65 | import org.neo4j.cypherdsl.core.StatementBuilder.OngoingReadingWithoutWhere; 66 | import org.neo4j.cypherdsl.core.renderer.Configuration; 67 | import org.neo4j.cypherdsl.core.renderer.Renderer; 68 | 69 | /** 70 | * Quick proof of concept of a jOOQ/Cypher-DSL based SQL to Cypher translator. 71 | * 72 | * @author Lukas Eder 73 | * @author Michael J. Simons 74 | * @author Michael Hunger 75 | */ 76 | public final class Translator { 77 | 78 | public static Translator defaultTranslator() { 79 | return new Translator(TranslatorConfig.defaultConfig()); 80 | } 81 | 82 | public static Translator with(TranslatorConfig config) { 83 | return new Translator(config); 84 | } 85 | 86 | private final TranslatorConfig config; 87 | 88 | private Translator(TranslatorConfig config) { 89 | 90 | this.config = config; 91 | } 92 | 93 | // Unsure how thread safe this should be (wrt the node lookup table), but this here 94 | // will do for the purpose of adding some tests 95 | public String convert(String sql) { 96 | Query query = parse(sql); 97 | 98 | if (query instanceof Select s) { 99 | return render(statement(s)); 100 | } 101 | else if (query instanceof QOM.Delete d) { 102 | return render(statement(d)); 103 | } 104 | else if (query instanceof QOM.Truncate t) { 105 | return render(statement(t)); 106 | } 107 | else if (query instanceof QOM.Insert t) { 108 | return render(statement(t)); 109 | } 110 | else { 111 | throw unsupported(query); 112 | } 113 | } 114 | 115 | @SuppressWarnings("ResultOfMethodCallIgnored") 116 | private DSLContext createDSLContext() { 117 | 118 | var settings = new DefaultConfiguration().settings().withParseNameCase(this.config.getParseNameCase()) 119 | .withRenderNameCase(this.config.getRenderNameCase()) 120 | .withParseWithMetaLookups(ParseWithMetaLookups.IGNORE_ON_FAILURE).withDiagnosticsLogging(true) 121 | .withParseDialect(this.config.getSqlDialect()); 122 | 123 | Optional.ofNullable(this.config.getParseNamedParamPrefix()).filter(Predicate.not(String::isBlank)) 124 | .map(String::trim).ifPresent(settings::withParseNamedParamPrefix); 125 | 126 | var context = DSL.using(this.config.getSqlDialect(), settings); 127 | context.configuration().set(() -> { 128 | var tables = new HashMap(); 129 | 130 | this.config.getJoinColumnsToTypeMappings().forEach((k, v) -> { 131 | var tableAndColumnName = k.split("\\."); 132 | var createTableStep = (CreateTableElementListStep) tables.computeIfAbsent(tableAndColumnName[0], 133 | DSL::createTable); 134 | createTableStep.column(DSL.field(tableAndColumnName[1]).comment("type=" + v)); 135 | }); 136 | 137 | this.config.getTableToLabelMappings().forEach((k, v) -> { 138 | var createTableStep = (CreateTableElementListStep) tables.computeIfAbsent(k, DSL::createTable); 139 | createTableStep.comment("label=" + v); 140 | }); 141 | 142 | return context.meta(tables.values().toArray(Query[]::new)); 143 | }); 144 | return context; 145 | } 146 | 147 | private Query parse(String sql) { 148 | DSLContext dsl = createDSLContext(); 149 | Parser parser = dsl.parser(); 150 | return parser.parseQuery(sql); 151 | } 152 | 153 | private String render(Statement statement) { 154 | Renderer renderer = Renderer.getRenderer( 155 | this.config.isPrettyPrint() ? Configuration.prettyPrinting() : Configuration.defaultConfig()); 156 | return renderer.render(statement); 157 | } 158 | 159 | Statement statement(QOM.Delete d) { 160 | Node e = (Node) resolveTableOrJoin(d.$from()); 161 | 162 | OngoingReadingWithoutWhere m1 = Cypher.match(e); 163 | OngoingReadingWithWhere m2 = (d.$where() != null) ? m1.where(condition(d.$where())) 164 | : (OngoingReadingWithWhere) m1; 165 | return m2.delete(e.asExpression()).build(); 166 | } 167 | 168 | Statement statement(QOM.Truncate t) { 169 | Node e = (Node) resolveTableOrJoin(t.$table()); 170 | 171 | return Cypher.match(e).detachDelete(e.asExpression()).build(); 172 | } 173 | 174 | ResultStatement statement(Select x) { 175 | 176 | // Done lazy as otherwise the property containers won't be resolved 177 | Supplier> resultColumnsSupplier = () -> x.$select().stream() 178 | .map((t) -> (Expression) expression(t)).toList(); 179 | 180 | if (x.$from().isEmpty()) { 181 | return Cypher.returning(resultColumnsSupplier.get()).build(); 182 | } 183 | 184 | OngoingReadingWithoutWhere m1 = Cypher.match(x.$from().stream().map(this::resolveTableOrJoin).toList()); 185 | 186 | OngoingReadingWithWhere m2 = (x.$where() != null) ? m1.where(condition(x.$where())) 187 | : (OngoingReadingWithWhere) m1; 188 | 189 | var returning = m2.returning(resultColumnsSupplier.get()) 190 | .orderBy(x.$orderBy().stream().map(this::expression).toList()); 191 | 192 | StatementBuilder.BuildableStatement buildableStatement; 193 | if (!(x.$limit() instanceof Param param)) { 194 | buildableStatement = returning; 195 | } 196 | else { 197 | buildableStatement = returning.limit(expression(param)); 198 | } 199 | 200 | return buildableStatement.build(); 201 | } 202 | 203 | Statement statement(QOM.Insert insert) { 204 | var table = insert.$into(); 205 | // TODO handle if this resolves to something unexpectedly different 206 | var node = (Node) this.resolveTableOrJoin(table); 207 | 208 | var rows = insert.$values(); 209 | var columns = insert.$columns(); 210 | 211 | if (rows.size() == 1) { 212 | Object[] keysAndValues = new Object[columns.size() * 2]; 213 | var row = rows.get(0); 214 | for (int i = 0; i < columns.size(); ++i) { 215 | keysAndValues[i * 2] = columns.get(i).getName(); 216 | keysAndValues[i * 2 + 1] = expression(row.field(i)); 217 | } 218 | return Cypher.create(node.withProperties(keysAndValues)).build(); 219 | } 220 | else { 221 | var props = insert.$values().stream().map(row -> { 222 | var result = new HashMap(columns.size()); 223 | for (int i = 0; i < columns.size(); ++i) { 224 | result.put(columns.get(i).getName(), expression(row.field(i))); 225 | } 226 | return Cypher.literalOf(result); 227 | }).toList(); 228 | return Cypher.unwind(Cypher.listOf(props)).as("properties").create(node) 229 | .set(node, Cypher.name("properties")).build(); 230 | } 231 | } 232 | 233 | private Expression expression(SelectFieldOrAsterisk t) { 234 | if (t instanceof SelectField s) { 235 | return expression(s); 236 | } 237 | else if (t instanceof Asterisk) { 238 | return Cypher.asterisk(); 239 | } 240 | else if (t instanceof QualifiedAsterisk q && resolveTableOrJoin(q.$table()) instanceof Node node) { 241 | return node.getSymbolicName().orElseGet(() -> Cypher.name(q.$table().getName())); 242 | } 243 | else { 244 | throw unsupported(t); 245 | } 246 | } 247 | 248 | private Expression expression(SelectField s) { 249 | if (s instanceof QOM.FieldAlias fa) { 250 | return expression(fa.$aliased()).as(fa.$alias().last()); 251 | } 252 | else if (s instanceof Field f) { 253 | return expression(f); 254 | } 255 | else { 256 | throw unsupported(s); 257 | } 258 | } 259 | 260 | private SortItem expression(SortField s) { 261 | return Cypher.sort(expression(s.$field()), 262 | SortItem.Direction.valueOf(s.$sortOrder().name().toUpperCase(Locale.ROOT))); 263 | } 264 | 265 | private Expression expression(Field f) { 266 | if (f instanceof Param p) { 267 | if (p.$inline()) { 268 | return Cypher.literalOf(p.getValue()); 269 | } 270 | else if (p.getParamName() != null) { 271 | return Cypher.parameter(p.getParamName(), p.getValue()); 272 | } 273 | else { 274 | return Cypher.anonParameter(p.getValue()); 275 | } 276 | } 277 | else if (f instanceof TableField tf && tf.getTable() != null) { 278 | var pe = resolveTableOrJoin(tf.getTable()); 279 | if (pe instanceof Node node) { 280 | return node.property(tf.getName()); 281 | } 282 | else if (pe instanceof Relationship rel) { 283 | return rel.property(tf.getName()); 284 | } 285 | else { 286 | throw unsupported(tf); 287 | } 288 | } 289 | else if (f instanceof QOM.Add e) { 290 | return expression(e.$arg1()).add(expression(e.$arg2())); 291 | } 292 | else if (f instanceof QOM.Sub e) { 293 | return expression(e.$arg1()).subtract(expression(e.$arg2())); 294 | } 295 | else if (f instanceof QOM.Mul e) { 296 | return expression(e.$arg1()).multiply(expression(e.$arg2())); 297 | } 298 | else if (f instanceof QOM.Square e) { 299 | return expression(e.$arg1()).multiply(expression(e.$arg1())); 300 | } 301 | else if (f instanceof QOM.Div e) { 302 | return expression(e.$arg1()).divide(expression(e.$arg2())); 303 | } 304 | else if (f instanceof QOM.Neg e) { 305 | throw unsupported(e); 306 | } 307 | 308 | // https://neo4j.com/docs/cypher-manual/current/functions/mathematical-numeric/ 309 | else if (f instanceof QOM.Abs e) { 310 | return org.neo4j.cypherdsl.core.Functions.abs(expression(e.$arg1())); 311 | } 312 | else if (f instanceof QOM.Ceil e) { 313 | return org.neo4j.cypherdsl.core.Functions.ceil(expression(e.$arg1())); 314 | } 315 | else if (f instanceof QOM.Floor e) { 316 | return org.neo4j.cypherdsl.core.Functions.floor(expression(e.$arg1())); 317 | } 318 | else if (f instanceof QOM.Round e) { 319 | if (e.$arg2() == null) { 320 | return org.neo4j.cypherdsl.core.Functions.round(expression(e.$arg1())); 321 | } 322 | else { 323 | return org.neo4j.cypherdsl.core.Functions.round(expression(e.$arg1()), expression(e.$arg2())); 324 | } 325 | } 326 | else if (f instanceof QOM.Sign e) { 327 | return org.neo4j.cypherdsl.core.Functions.sign(expression(e.$arg1())); 328 | } 329 | else if (f instanceof QOM.Rand) { 330 | return Functions.rand(); 331 | } 332 | 333 | // https://neo4j.com/docs/cypher-manual/current/functions/mathematical-logarithmic/ 334 | else if (f instanceof QOM.Euler) { 335 | return Functions.e(); 336 | } 337 | else if (f instanceof QOM.Exp e) { 338 | return Functions.exp(expression(e.$arg1())); 339 | } 340 | else if (f instanceof QOM.Ln e) { 341 | return org.neo4j.cypherdsl.core.Functions.log(expression(e.$arg1())); 342 | } 343 | else if (f instanceof QOM.Log e) { 344 | return org.neo4j.cypherdsl.core.Functions.log(expression(e.$arg1())) 345 | .divide(org.neo4j.cypherdsl.core.Functions.log(expression(e.$arg2()))); 346 | } 347 | else if (f instanceof QOM.Log10 e) { 348 | return org.neo4j.cypherdsl.core.Functions.log10(expression(e.$arg1())); 349 | } 350 | else if (f instanceof QOM.Sqrt e) { 351 | return org.neo4j.cypherdsl.core.Functions.sqrt(expression(e.$arg1())); 352 | } 353 | // TODO: Hyperbolic functions 354 | 355 | // https://neo4j.com/docs/cypher-manual/current/functions/mathematical-trigonometric/ 356 | else if (f instanceof QOM.Acos e) { 357 | return org.neo4j.cypherdsl.core.Functions.acos(expression(e.$arg1())); 358 | } 359 | else if (f instanceof QOM.Asin e) { 360 | return org.neo4j.cypherdsl.core.Functions.asin(expression(e.$arg1())); 361 | } 362 | else if (f instanceof QOM.Atan e) { 363 | return org.neo4j.cypherdsl.core.Functions.atan(expression(e.$arg1())); 364 | } 365 | else if (f instanceof QOM.Atan2 e) { 366 | return org.neo4j.cypherdsl.core.Functions.atan2(expression(e.$arg1()), expression(e.$arg2())); 367 | } 368 | else if (f instanceof QOM.Cos e) { 369 | return org.neo4j.cypherdsl.core.Functions.cos(expression(e.$arg1())); 370 | } 371 | else if (f instanceof QOM.Cot e) { 372 | return org.neo4j.cypherdsl.core.Functions.cot(expression(e.$arg1())); 373 | } 374 | else if (f instanceof QOM.Degrees e) { 375 | return org.neo4j.cypherdsl.core.Functions.degrees(expression(e.$arg1())); 376 | } 377 | else if (f instanceof QOM.Pi) { 378 | return Functions.pi(); 379 | } 380 | else if (f instanceof QOM.Radians e) { 381 | return org.neo4j.cypherdsl.core.Functions.radians(expression(e.$arg1())); 382 | } 383 | else if (f instanceof QOM.Sin e) { 384 | return org.neo4j.cypherdsl.core.Functions.sin(expression(e.$arg1())); 385 | } 386 | else if (f instanceof QOM.Tan e) { 387 | return org.neo4j.cypherdsl.core.Functions.tan(expression(e.$arg1())); 388 | } 389 | 390 | // https://neo4j.com/docs/cypher-manual/current/functions/string/ 391 | else if (f instanceof QOM.CharLength e) { 392 | return Functions.size(expression(e.$arg1())); 393 | } 394 | else if (f instanceof QOM.Left e) { 395 | return Functions.left(expression(e.$arg1()), expression(e.$arg2())); 396 | } 397 | else if (f instanceof QOM.Lower e) { 398 | return org.neo4j.cypherdsl.core.Functions.toLower(expression(e.$arg1())); 399 | } 400 | else if (f instanceof QOM.Ltrim e) { 401 | return org.neo4j.cypherdsl.core.Functions.ltrim(expression(e.$arg1())); 402 | } 403 | else if (f instanceof QOM.Replace e) { 404 | return Functions.replace(expression(e.$arg1()), expression(e.$arg2()), expression(e.$arg3())); 405 | } 406 | else if (f instanceof QOM.Reverse e) { 407 | return Functions.reverse(expression(e.$arg1())); 408 | } 409 | else if (f instanceof QOM.Right e) { 410 | return Functions.right(expression(e.$arg1()), expression(e.$arg2())); 411 | } 412 | else if (f instanceof QOM.Rtrim e) { 413 | return org.neo4j.cypherdsl.core.Functions.rtrim(expression(e.$arg1())); 414 | } 415 | else if (f instanceof QOM.Substring e) { 416 | var length = expression(e.$arg3()); 417 | if (length != Cypher.literalNull()) { 418 | return org.neo4j.cypherdsl.core.Functions.substring(expression(e.$arg1()), expression(e.$arg2()), 419 | length); 420 | } 421 | else { 422 | return org.neo4j.cypherdsl.core.Functions.substring(expression(e.$arg1()), expression(e.$arg2()), null); 423 | } 424 | } 425 | else if (f instanceof QOM.Trim e) { 426 | if (e.$arg2() != null) { 427 | throw unsupported(e); 428 | } 429 | else { 430 | return Functions.trim(expression(e.$arg1())); 431 | } 432 | } 433 | else if (f instanceof QOM.Upper e) { 434 | return Functions.toUpper(expression(e.$arg1())); 435 | } 436 | 437 | // https://neo4j.com/docs/cypher-manual/current/functions/scalar/ 438 | else if (f instanceof QOM.Coalesce e) { 439 | return Functions.coalesce(e.$arg1().stream().map(this::expression).toArray(Expression[]::new)); 440 | } 441 | else if (f instanceof QOM.Nvl e) { 442 | return Functions.coalesce(expression(e.$arg1()), expression(e.$arg2())); 443 | } 444 | 445 | // https://neo4j.com/docs/cypher-manual/current/syntax/expressions/ 446 | else if (f instanceof QOM.Nullif e) { 447 | return Cypher.caseExpression().when(expression(e.$arg1()).eq(expression(e.$arg2()))) 448 | .then(Cypher.literalNull()).elseDefault(expression(e.$arg1())); 449 | } 450 | else if (f instanceof QOM.Nvl2 e) { 451 | return Cypher.caseExpression().when(expression(e.$arg1()).isNotNull()).then(expression(e.$arg2())) 452 | .elseDefault(expression(e.$arg3())); 453 | } 454 | else if (f instanceof QOM.CaseSimple e) { 455 | Case c = Cypher.caseExpression(expression(e.$value())); 456 | 457 | for (var w : e.$when()) { 458 | c = c.when(expression(w.$1())).then(expression(w.$2())); 459 | } 460 | 461 | if (e.$else() != null) { 462 | c = ((Case.CaseEnding) c).elseDefault(expression(e.$else())); 463 | } 464 | 465 | return c; 466 | } 467 | else if (f instanceof QOM.CaseSearched e) { 468 | Case c = Cypher.caseExpression(); 469 | 470 | for (var w : e.$when()) { 471 | c = c.when(condition(w.$1())).then(expression(w.$2())); 472 | } 473 | 474 | if (e.$else() != null) { 475 | c = ((Case.CaseEnding) c).elseDefault(expression(e.$else())); 476 | } 477 | 478 | return c; 479 | } 480 | 481 | // Others 482 | else if (f instanceof QOM.Cast e) { 483 | if (e.$dataType().isString()) { 484 | return Functions.toString(expression(e.$field())); 485 | } 486 | else if (e.$dataType().isBoolean()) { 487 | return Functions.toBoolean(expression(e.$field())); 488 | } 489 | else if (e.$dataType().isFloat()) { 490 | return Functions.toFloat(expression(e.$field())); 491 | } 492 | else if (e.$dataType().isInteger()) { 493 | return Functions.toInteger(expression(e.$field())); 494 | } 495 | else { 496 | throw unsupported(f); 497 | } 498 | } 499 | else if (f instanceof org.jooq.True) { 500 | return Cypher.literalTrue(); 501 | } 502 | else if (f instanceof org.jooq.False) { 503 | return Cypher.literalFalse(); 504 | } 505 | else if (f instanceof QOM.Null || f == null || f instanceof org.jooq.Null) { 506 | return Cypher.literalNull(); 507 | } 508 | else { 509 | throw unsupported(f); 510 | } 511 | } 512 | 513 | private IllegalArgumentException unsupported(QueryPart p) { 514 | return new IllegalArgumentException("Unsupported SQL expression: " + p); 515 | } 516 | 517 | private Condition condition(org.jooq.Condition c) { 518 | if (c instanceof QOM.And a) { 519 | return condition(a.$arg1()).and(condition(a.$arg2())); 520 | } 521 | else if (c instanceof QOM.Or o) { 522 | return condition(o.$arg1()).or(condition(o.$arg2())); 523 | } 524 | else if (c instanceof QOM.Xor o) { 525 | return condition(o.$arg1()).xor(condition(o.$arg2())); 526 | } 527 | else if (c instanceof QOM.Not o) { 528 | return condition(o.$arg1()).not(); 529 | } 530 | else if (c instanceof QOM.Eq e) { 531 | return expression(e.$arg1()).eq(expression(e.$arg2())); 532 | } 533 | else if (c instanceof QOM.Gt e) { 534 | return expression(e.$arg1()).gt(expression(e.$arg2())); 535 | } 536 | else if (c instanceof QOM.Ge e) { 537 | return expression(e.$arg1()).gte(expression(e.$arg2())); 538 | } 539 | else if (c instanceof QOM.Lt e) { 540 | return expression(e.$arg1()).lt(expression(e.$arg2())); 541 | } 542 | else if (c instanceof QOM.Le e) { 543 | return expression(e.$arg1()).lte(expression(e.$arg2())); 544 | } 545 | else if (c instanceof QOM.Between e) { 546 | if (e.$symmetric()) { 547 | @SuppressWarnings("unchecked") 548 | QOM.Between t = (QOM.Between) e; 549 | return condition(t.$symmetric(false)) 550 | .or(condition(t.$symmetric(false).$arg2(t.$arg3()).$arg3(t.$arg2()))); 551 | } 552 | else { 553 | return expression(e.$arg2()).lte(expression(e.$arg1())) 554 | .and(expression(e.$arg1()).lte(expression(e.$arg3()))); 555 | } 556 | } 557 | else if (c instanceof QOM.Ne e) { 558 | return expression(e.$arg1()).ne(expression(e.$arg2())); 559 | } 560 | else if (c instanceof QOM.IsNull e) { 561 | return expression(e.$arg1()).isNull(); 562 | } 563 | else if (c instanceof QOM.IsNotNull e) { 564 | return expression(e.$arg1()).isNotNull(); 565 | } 566 | else if (c instanceof QOM.RowEq e) { 567 | Condition result = null; 568 | 569 | for (int i = 0; i < e.$arg1().size(); i++) { 570 | Condition r = expression(e.$arg1().field(i)).eq(expression(e.$arg2().field(i))); 571 | result = (result != null) ? result.and(r) : r; 572 | } 573 | 574 | return result; 575 | } 576 | else if (c instanceof QOM.RowNe e) { 577 | Condition result = null; 578 | 579 | for (int i = 0; i < e.$arg1().size(); i++) { 580 | Condition r = expression(e.$arg1().field(i)).ne(expression(e.$arg2().field(i))); 581 | result = (result != null) ? result.and(r) : r; 582 | } 583 | 584 | return result; 585 | } 586 | else if (c instanceof QOM.RowGt e) { 587 | return rowCondition(e.$arg1(), e.$arg2(), Expression::gt, Expression::gt); 588 | } 589 | else if (c instanceof QOM.RowGe e) { 590 | return rowCondition(e.$arg1(), e.$arg2(), Expression::gt, Expression::gte); 591 | } 592 | else if (c instanceof QOM.RowLt e) { 593 | return rowCondition(e.$arg1(), e.$arg2(), Expression::lt, Expression::lt); 594 | } 595 | else if (c instanceof QOM.RowLe e) { 596 | return rowCondition(e.$arg1(), e.$arg2(), Expression::lt, Expression::lte); 597 | } 598 | else if (c instanceof QOM.RowIsNull e) { 599 | return e.$arg1().$fields().stream().map(f -> expression(f).isNull()).reduce(Condition::and).get(); 600 | } 601 | else if (c instanceof QOM.RowIsNotNull e) { 602 | return e.$arg1().$fields().stream().map(f -> expression(f).isNotNull()).reduce(Condition::and).get(); 603 | } 604 | else if (c instanceof QOM.Like like) { 605 | Expression rhs; 606 | if (like.$arg2() instanceof Param p && p.$inline() && p.getValue() instanceof String s) { 607 | rhs = Cypher.literalOf(s.replace("%", ".*")); 608 | } 609 | else { 610 | rhs = expression(like.$arg2()); 611 | } 612 | return expression(like.$arg1()).matches(rhs); 613 | } 614 | else { 615 | throw unsupported(c); 616 | } 617 | } 618 | 619 | private Condition rowCondition(Row r1, Row r2, 620 | BiFunction comp, 621 | BiFunction last) { 622 | Condition result = last.apply(expression(r1.field(r1.size() - 1)), expression(r2.field(r1.size() - 1))); 623 | 624 | for (int i = r1.size() - 2; i >= 0; i--) { 625 | Expression e1 = expression(r1.field(i)); 626 | Expression e2 = expression(r2.field(i)); 627 | result = comp.apply(e1, e2).or(e1.eq(e2).and(result)); 628 | } 629 | 630 | return result; 631 | } 632 | 633 | private PatternElement resolveTableOrJoin(Table t) { 634 | if (t instanceof QOM.Join join && join.$on() instanceof QOM.Eq eq) { 635 | 636 | String relType; 637 | String relSymbolicName = null; 638 | 639 | PatternElement lhs; 640 | PatternElement rhs; 641 | 642 | if (join.$table1() instanceof QOM.Join lhsJoin) { 643 | lhs = resolveTableOrJoin(lhsJoin.$table1()); 644 | relType = labelOrType(lhsJoin.$table2()); 645 | if (lhsJoin.$table2() instanceof TableAlias tableAlias) { 646 | relSymbolicName = tableAlias.getName(); 647 | } 648 | } 649 | else { 650 | lhs = resolveTableOrJoin(join.$table1()); 651 | relType = relationshipTypeName(eq.$arg2()); 652 | } 653 | 654 | rhs = resolveTableOrJoin(join.$table2()); 655 | 656 | if (lhs instanceof ExposesRelationships from && rhs instanceof Node to) { 657 | 658 | var direction = Relationship.Direction.LTR; 659 | if (join.$table2() instanceof TableAlias ta 660 | && ta.$alias().last().equals(eq.$arg2().getQualifiedName().first())) { 661 | direction = Relationship.Direction.RTL; 662 | } 663 | 664 | var relationship = from.relationshipWith(to, direction, relType); 665 | if (relSymbolicName != null) { 666 | if (relationship instanceof Relationship r) { 667 | relationship = r.named(relSymbolicName); 668 | } 669 | else if (relationship instanceof RelationshipChain r) { 670 | relationship = r.named(relSymbolicName); 671 | } 672 | } 673 | return relationship; 674 | } 675 | else { 676 | throw unsupported(join); 677 | } 678 | } 679 | 680 | if (t instanceof TableAlias ta) { 681 | if (resolveTableOrJoin(ta.$aliased()) instanceof Node) { 682 | return Cypher.node(labelOrType(ta.$aliased())).named(ta.$alias().last()); 683 | } 684 | else { 685 | throw unsupported(ta); 686 | } 687 | } 688 | else { 689 | return Cypher.node(labelOrType(t)).named(t.getName()); 690 | } 691 | } 692 | 693 | private String labelOrType(Table tableOrAlias) { 694 | 695 | var t = (tableOrAlias instanceof TableAlias ta) ? ta.$aliased() : tableOrAlias; 696 | var comment = t.getComment(); 697 | if (!comment.isBlank()) { 698 | var config = Arrays.stream(comment.split(",")).map((s) -> s.split("=")) 699 | .collect(Collectors.toMap((a) -> a[0], (a) -> a[1])); 700 | return config.getOrDefault("label", t.getName()); 701 | } 702 | 703 | return t.getName(); 704 | } 705 | 706 | private String relationshipTypeName(Field lhsJoinColumn) { 707 | 708 | return Objects.requireNonNull(lhsJoinColumn.getQualifiedName().last()).toUpperCase(Locale.ROOT); 709 | } 710 | 711 | } 712 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/sql2cypher/TranslatorCLI.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.neo4j.sql2cypher; 17 | 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | 21 | import org.jooq.SQLDialect; 22 | import org.jooq.conf.ParseNameCase; 23 | import picocli.AutoComplete.GenerateCompletion; 24 | import picocli.CommandLine; 25 | import picocli.CommandLine.Command; 26 | import picocli.CommandLine.HelpCommand; 27 | import picocli.CommandLine.Option; 28 | import picocli.CommandLine.Parameters; 29 | 30 | /** 31 | * Main entry to the {@link Translator translator cli}. 32 | * 33 | * @author Michael J. Simons 34 | */ 35 | @SuppressWarnings({ "FieldMayBeFinal" }) 36 | @Command(name = "sql2cypher", mixinStandardHelpOptions = true, 37 | description = "Translates SQL statements to Cypher queries.", sortOptions = false, 38 | versionProvider = ManifestVersionProvider.class, subcommands = { GenerateCompletion.class, HelpCommand.class }) 39 | public final class TranslatorCLI implements Runnable { 40 | 41 | @Option(names = "--parse-name-case", 42 | description = "How to parse names; valid values are: ${COMPLETION-CANDIDATES} and the default is ${DEFAULT-VALUE}") 43 | private ParseNameCase parseNameCase = TranslatorConfig.defaultConfig().getParseNameCase(); 44 | 45 | @Option(names = "--table-to-label-mapping", 46 | description = "A table name that should be mapped to a specific label, repeat for multiple mappings") 47 | private Map tableToLabelMappings = new HashMap<>(); 48 | 49 | @Option(names = "--sql-dialect", 50 | description = "The SQL dialect to use for parsing; valid values are: ${COMPLETION-CANDIDATES} and the default is ${DEFAULT-VALUE}") 51 | private SQLDialect sqlDialect = TranslatorConfig.defaultConfig().getSqlDialect(); 52 | 53 | @Option(names = "--disable-pretty-printing", description = "Disables pretty printing") 54 | private boolean disablePrettyPrinting = false; 55 | 56 | @Parameters(index = "0", description = "Any valid SQL statement that should be translated to Cypher") 57 | private String sql; 58 | 59 | public static void main(String... args) { 60 | 61 | var commandLine = new CommandLine(new TranslatorCLI()).setCaseInsensitiveEnumValuesAllowed(true); 62 | 63 | var generateCompletionCmd = commandLine.getSubcommands().get("generate-completion"); 64 | generateCompletionCmd.getCommandSpec().usageMessage().hidden(true); 65 | 66 | int exitCode = commandLine.execute(args); 67 | System.exit(exitCode); 68 | } 69 | 70 | @Override 71 | public void run() { 72 | 73 | var cfg = TranslatorConfig.builder().withParseNameCase(this.parseNameCase) 74 | .withTableToLabelMappings(this.tableToLabelMappings).withSqlDialect(this.sqlDialect) 75 | .withPrettyPrint(!this.disablePrettyPrinting).build(); 76 | System.out.println(Translator.with(cfg).convert(this.sql)); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/sql2cypher/TranslatorConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.neo4j.sql2cypher; 17 | 18 | import java.util.Map; 19 | import java.util.Objects; 20 | 21 | import org.jooq.SQLDialect; 22 | import org.jooq.conf.ParseNameCase; 23 | import org.jooq.conf.RenderNameCase; 24 | 25 | /** 26 | * Configuration for the {@link Translator}, use this to configure parsing and rendering 27 | * settings as well as table to node mappings. 28 | * 29 | * @author Michael Hunger 30 | * @author Michael J. Simons 31 | */ 32 | public final class TranslatorConfig { 33 | 34 | private static final TranslatorConfig DEFAULT_CONFIG = TranslatorConfig.builder().build(); 35 | 36 | /** 37 | * A builder for creating new {@link TranslatorConfig configuration objects}. 38 | * @return a new builder for creating a new configuration from scratch. 39 | */ 40 | public static TranslatorConfig.Builder builder() { 41 | return new Builder(); 42 | } 43 | 44 | /** 45 | * Provides access to the default configuration. 46 | * @return the default configuration ready to use. 47 | */ 48 | public static TranslatorConfig defaultConfig() { 49 | 50 | return DEFAULT_CONFIG; 51 | } 52 | 53 | private final ParseNameCase parseNameCase; 54 | 55 | private final RenderNameCase renderNameCase; 56 | 57 | private final boolean jooqDiagnosticLogging; 58 | 59 | private final Map tableToLabelMappings; 60 | 61 | private final Map joinColumnsToTypeMappings; 62 | 63 | private final SQLDialect sqlDialect; 64 | 65 | private final boolean prettyPrint; 66 | 67 | private final String parseNamedParamPrefix; 68 | 69 | private TranslatorConfig(Builder builder) { 70 | 71 | this.parseNameCase = builder.parseNameCase; 72 | this.renderNameCase = builder.renderNameCase; 73 | this.jooqDiagnosticLogging = builder.jooqDiagnosticLogging; 74 | this.tableToLabelMappings = builder.tableToLabelMappings; 75 | this.joinColumnsToTypeMappings = builder.joinColumnsToTypeMappings; 76 | this.sqlDialect = builder.sqlDialect; 77 | this.prettyPrint = builder.prettyPrint; 78 | this.parseNamedParamPrefix = builder.parseNamedParamPrefix; 79 | } 80 | 81 | /** 82 | * Allows modifying this configuration. 83 | * @return builder with all settings from this instance 84 | */ 85 | public Builder modify() { 86 | return new Builder(this); 87 | } 88 | 89 | public ParseNameCase getParseNameCase() { 90 | return this.parseNameCase; 91 | } 92 | 93 | public RenderNameCase getRenderNameCase() { 94 | return this.renderNameCase; 95 | } 96 | 97 | public boolean isJooqDiagnosticLogging() { 98 | return this.jooqDiagnosticLogging; 99 | } 100 | 101 | public Map getTableToLabelMappings() { 102 | return this.tableToLabelMappings; 103 | } 104 | 105 | public Map getJoinColumnsToTypeMappings() { 106 | return this.joinColumnsToTypeMappings; 107 | } 108 | 109 | public SQLDialect getSqlDialect() { 110 | return this.sqlDialect; 111 | } 112 | 113 | public boolean isPrettyPrint() { 114 | return this.prettyPrint; 115 | } 116 | 117 | public String getParseNamedParamPrefix() { 118 | return this.parseNamedParamPrefix; 119 | } 120 | 121 | /** 122 | * A builder to create new instances of {@link TranslatorConfig configurations}. 123 | */ 124 | public static final class Builder { 125 | 126 | private ParseNameCase parseNameCase; 127 | 128 | private RenderNameCase renderNameCase; 129 | 130 | private boolean jooqDiagnosticLogging; 131 | 132 | private Map tableToLabelMappings; 133 | 134 | private Map joinColumnsToTypeMappings; 135 | 136 | private SQLDialect sqlDialect; 137 | 138 | private boolean prettyPrint; 139 | 140 | private String parseNamedParamPrefix; 141 | 142 | private Builder() { 143 | this(ParseNameCase.LOWER_IF_UNQUOTED, RenderNameCase.LOWER, false, Map.of(), Map.of(), SQLDialect.DEFAULT, 144 | true, null); 145 | } 146 | 147 | private Builder(TranslatorConfig config) { 148 | this(config.parseNameCase, config.renderNameCase, config.jooqDiagnosticLogging, config.tableToLabelMappings, 149 | config.joinColumnsToTypeMappings, config.sqlDialect, config.prettyPrint, 150 | config.parseNamedParamPrefix); 151 | } 152 | 153 | private Builder(ParseNameCase parseNameCase, RenderNameCase renderNameCase, boolean jooqDiagnosticLogging, 154 | Map tableToLabelMappings, Map joinColumnsToTypeMappings, 155 | SQLDialect sqlDialect, boolean prettyPrint, String parseNamedParamPrefix) { 156 | this.parseNameCase = parseNameCase; 157 | this.renderNameCase = renderNameCase; 158 | this.jooqDiagnosticLogging = jooqDiagnosticLogging; 159 | this.tableToLabelMappings = tableToLabelMappings; 160 | this.joinColumnsToTypeMappings = joinColumnsToTypeMappings; 161 | this.sqlDialect = sqlDialect; 162 | this.prettyPrint = prettyPrint; 163 | this.parseNamedParamPrefix = parseNamedParamPrefix; 164 | } 165 | 166 | /** 167 | * Configures how names should be parsed. 168 | * @param newParseNameCase the new configuration 169 | * @return this builder 170 | */ 171 | public Builder withParseNameCase(ParseNameCase newParseNameCase) { 172 | this.parseNameCase = Objects.requireNonNull(newParseNameCase); 173 | return this; 174 | } 175 | 176 | /** 177 | * Configures how SQL names should be parsed. 178 | * @param newRenderNameCase the new configuration 179 | * @return this builder 180 | */ 181 | public Builder withRenderNameCase(RenderNameCase newRenderNameCase) { 182 | this.renderNameCase = Objects.requireNonNull(newRenderNameCase); 183 | return this; 184 | } 185 | 186 | /** 187 | * Enables diagnostic logging for jOOQ. 188 | * @param enabled set to {@literal true} to enable diagnostic logging on the jOOQ 189 | * side of things 190 | * @return this builder 191 | */ 192 | public Builder withJooqDiagnosticLogging(boolean enabled) { 193 | this.jooqDiagnosticLogging = enabled; 194 | return this; 195 | } 196 | 197 | /** 198 | * Applies new table mappings. 199 | * @param newTableToLabelMappings the new mappings 200 | * @return this builder 201 | */ 202 | public Builder withTableToLabelMappings(Map newTableToLabelMappings) { 203 | this.tableToLabelMappings = Map.copyOf(Objects.requireNonNull(newTableToLabelMappings)); 204 | return this; 205 | } 206 | 207 | /** 208 | * Applies new join column mappings. 209 | * @param newJoinColumnsToTypeMappings the new mappings 210 | * @return this builder 211 | */ 212 | public Builder withJoinColumnsToTypeMappings(Map newJoinColumnsToTypeMappings) { 213 | this.joinColumnsToTypeMappings = Map.copyOf(Objects.requireNonNull(newJoinColumnsToTypeMappings)); 214 | return this; 215 | } 216 | 217 | /** 218 | * Applies a new {@link SQLDialect} for both parsing and optionally rendering SQL. 219 | * @param newSqlDialect the new sql dialect 220 | * @return this builder 221 | */ 222 | public Builder withSqlDialect(SQLDialect newSqlDialect) { 223 | this.sqlDialect = Objects.requireNonNull(newSqlDialect); 224 | return this; 225 | } 226 | 227 | /** 228 | * Enables or disables pretty printing of the generated Cypher queries. 229 | * @param prettyPrint set to {@literal false} to disable pretty printing 230 | * @return this builder 231 | */ 232 | public Builder withPrettyPrint(boolean prettyPrint) { 233 | this.prettyPrint = prettyPrint; 234 | return this; 235 | } 236 | 237 | /** 238 | * Changes the prefix used for parsing named parameters. If set to 239 | * {@literal null}, the jOOQ default ({@literal :}) is used. 240 | * @param parseNamedParamPrefix the new prefix for parsing named parameters 241 | * @return this builder 242 | */ 243 | public Builder withParseNamedParamPrefix(String parseNamedParamPrefix) { 244 | this.parseNamedParamPrefix = parseNamedParamPrefix; 245 | return this; 246 | } 247 | 248 | /** 249 | * Finishes building a new configuration. The builder is safe to reuse afterwards. 250 | * @return a new immutable configuration 251 | */ 252 | public TranslatorConfig build() { 253 | return new TranslatorConfig(this); 254 | } 255 | 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/main/java/org/neo4j/sql2cypher/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * Contains the sql2cypher translator and a CLI application. 19 | */ 20 | package org.neo4j.sql2cypher; 21 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/org.example/jOOQ-cypher/native-image.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2023 the original author or authors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # https://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | Args = -H:ReflectionConfigurationResources=${.}/reflection-config.json \ 18 | -H:ResourceConfigurationResources=${.}/resources-config.json \ 19 | --initialize-at-run-time=org.jooq 20 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/org.example/jOOQ-cypher/reflection-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "org.jooq.impl.SQLDataType", 4 | "allPublicConstructors": true 5 | }, 6 | { 7 | "name": "org.jooq.util.cubrid.CUBRIDDataType" 8 | }, 9 | { 10 | "name": "org.jooq.util.derby.DerbyDataType" 11 | }, 12 | { 13 | "name": "org.jooq.util.firebird.FirebirdDataType" 14 | }, 15 | { 16 | "name": "org.jooq.util.h2.H2DataType" 17 | }, 18 | { 19 | "name": "org.jooq.util.hsqldb.HSQLDBDataType" 20 | }, 21 | { 22 | "name": "org.jooq.util.ignite.IgniteDataType" 23 | }, 24 | { 25 | "name": "org.jooq.util.mariadb.MariaDBDataType" 26 | }, 27 | { 28 | "name": "org.jooq.util.mysql.MySQLDataType" 29 | }, 30 | { 31 | "name": "org.jooq.util.postgres.PostgresDataType" 32 | }, 33 | { 34 | "name": "org.jooq.util.sqlite.SQLiteDataType" 35 | }, 36 | { 37 | "name": "org.jooq.util.yugabytedb.YugabyteDBDataType" 38 | }, 39 | { 40 | "name": "java.lang.Boolean[]", 41 | "allPublicConstructors": true 42 | }, 43 | { 44 | "name": "java.lang.Byte[]", 45 | "allPublicConstructors": true 46 | }, 47 | { 48 | "name": "java.lang.Double[]", 49 | "allPublicConstructors": true 50 | }, 51 | { 52 | "name": "java.lang.Float[]", 53 | "allPublicConstructors": true 54 | }, 55 | { 56 | "name": "java.lang.Integer[]", 57 | "allPublicConstructors": true 58 | }, 59 | { 60 | "name": "java.lang.Long[]", 61 | "allPublicConstructors": true 62 | }, 63 | { 64 | "name": "java.lang.Object[]", 65 | "allPublicConstructors": true 66 | }, 67 | { 68 | "name": "java.lang.Short[]", 69 | "allPublicConstructors": true 70 | }, 71 | { 72 | "name": "java.lang.String[]", 73 | "allPublicConstructors": true 74 | }, 75 | { 76 | "name": "java.math.BigDecimal[]", 77 | "allPublicConstructors": true 78 | }, 79 | { 80 | "name": "java.math.BigInteger[]", 81 | "allPublicConstructors": true 82 | }, 83 | { 84 | "name": "java.sql.Date[]", 85 | "allPublicConstructors": true 86 | }, 87 | { 88 | "name": "java.sql.Time[]", 89 | "allPublicConstructors": true 90 | }, 91 | { 92 | "name": "java.sql.Timestamp[]", 93 | "allPublicConstructors": true 94 | }, 95 | { 96 | "name": "java.time.Instant[]", 97 | "allPublicConstructors": true 98 | }, 99 | { 100 | "name": "java.time.LocalDateTime[]", 101 | "allPublicConstructors": true 102 | }, 103 | { 104 | "name": "java.time.LocalDate[]", 105 | "allPublicConstructors": true 106 | }, 107 | { 108 | "name": "java.time.LocalTime[]", 109 | "allPublicConstructors": true 110 | }, 111 | { 112 | "name": "java.time.OffsetDateTime[]", 113 | "allPublicConstructors": true 114 | }, 115 | { 116 | "name": "java.time.OffsetTime[]", 117 | "allPublicConstructors": true 118 | }, 119 | { 120 | "name": "java.time.Year[]", 121 | "allPublicConstructors": true 122 | }, 123 | { 124 | "name": "java.util.UUID[]", 125 | "allPublicConstructors": true 126 | }, 127 | { 128 | "name": "org.jooq.Geography[]", 129 | "allPublicConstructors": true 130 | }, 131 | { 132 | "name": "org.jooq.Geometry[]", 133 | "allPublicConstructors": true 134 | }, 135 | { 136 | "name": "org.jooq.JSONB[]", 137 | "allPublicConstructors": true 138 | }, 139 | { 140 | "name": "org.jooq.JSON[]", 141 | "allPublicConstructors": true 142 | }, 143 | { 144 | "name": "org.jooq.XML[]", 145 | "allPublicConstructors": true 146 | }, 147 | { 148 | "name": "org.jooq.types.DayToSecond[]", 149 | "allPublicConstructors": true 150 | }, 151 | { 152 | "name": "org.jooq.types.UByte[]", 153 | "allPublicConstructors": true 154 | }, 155 | { 156 | "name": "org.jooq.types.UInteger[]", 157 | "allPublicConstructors": true 158 | }, 159 | { 160 | "name": "org.jooq.types.ULong[]", 161 | "allPublicConstructors": true 162 | }, 163 | { 164 | "name": "org.jooq.types.UShort[]", 165 | "allPublicConstructors": true 166 | }, 167 | { 168 | "name": "org.jooq.types.YearToMonth[]", 169 | "allPublicConstructors": true 170 | }, 171 | { 172 | "name": "org.jooq.types.YearToSecond[]", 173 | "allPublicConstructors": true 174 | }, 175 | { 176 | "name": "org.jooq.Record[]", 177 | "allPublicConstructors": true 178 | }, 179 | { 180 | "name": "org.jooq.Result[]", 181 | "allPublicConstructors": true 182 | }, 183 | { 184 | "name": "org.jooq.RowId[]", 185 | "allPublicConstructors": true 186 | } 187 | ] 188 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/native-image/org.example/jOOQ-cypher/resources-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | { 4 | "pattern": "META-INF/MANIFEST\\.MF" 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /src/test/java/org/neo4j/sql2cypher/TranslatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.neo4j.sql2cypher; 17 | 18 | import java.io.File; 19 | import java.io.IOException; 20 | import java.nio.file.Files; 21 | import java.nio.file.Path; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.HashMap; 25 | import java.util.List; 26 | import java.util.Map; 27 | import java.util.function.Function; 28 | import java.util.stream.Collectors; 29 | import java.util.stream.Stream; 30 | 31 | import org.asciidoctor.Asciidoctor; 32 | import org.asciidoctor.Options; 33 | import org.asciidoctor.ast.Block; 34 | import org.asciidoctor.ast.ContentNode; 35 | import org.asciidoctor.ast.Document; 36 | import org.asciidoctor.extension.Treeprocessor; 37 | import org.junit.jupiter.api.DynamicContainer; 38 | import org.junit.jupiter.api.DynamicTest; 39 | import org.junit.jupiter.api.Test; 40 | import org.junit.jupiter.api.TestFactory; 41 | import org.neo4j.cypherdsl.parser.CypherParser; 42 | 43 | import static org.assertj.core.api.Assertions.assertThat; 44 | 45 | /** 46 | * @author Michael J. Simons 47 | * @author Michael Hunger 48 | */ 49 | class TranslatorTest { 50 | 51 | @Test 52 | void namedParameterPrefixForParsingShouldBeConfigurable() { 53 | var translator = Translator 54 | .with(TranslatorConfig.builder().withParseNamedParamPrefix("$").withPrettyPrint(false).build()); 55 | assertThat(translator.convert("INSERT INTO Movie (Movie.title) VALUES($1)")) 56 | .isEqualTo("CREATE (movie:`movie` {title: $1})"); 57 | } 58 | 59 | static List getTestData(Path path) { 60 | try (var asciidoctor = Asciidoctor.Factory.create()) { 61 | var collector = new TestDataExtractor(); 62 | asciidoctor.javaExtensionRegistry().treeprocessor(collector); 63 | 64 | var content = Files.readString(path); 65 | asciidoctor.load(content, Options.builder().build()); 66 | return collector.testData; 67 | } 68 | catch (IOException ioe) { 69 | throw new RuntimeException("Error reading TCK file " + path, ioe); 70 | } 71 | } 72 | 73 | @TestFactory 74 | Stream tck() { 75 | 76 | var path = ClassLoader.getSystemResource("simple.adoc").getPath(); 77 | var parentFolder = new File(path).getParentFile(); 78 | if (!parentFolder.isDirectory()) { 79 | return Stream.empty(); 80 | } 81 | 82 | var files = parentFolder.listFiles((f) -> f.getName().toLowerCase().endsWith(".adoc")); 83 | if (files == null) { 84 | return Stream.empty(); 85 | } 86 | 87 | return Arrays.stream(files).map(file -> { 88 | var tests = getTestData(file.toPath()).stream() 89 | .map(t -> DynamicTest.dynamicTest(t.name, 90 | () -> assertThatSqlIsTranslatedAsExpected(t.sql, t.cypher, t.tableMappings, t.prettyPrint))) 91 | .toList(); 92 | return DynamicContainer.dynamicContainer(file.getName(), tests); 93 | }); 94 | } 95 | 96 | void assertThatSqlIsTranslatedAsExpected(String sql, String expected, Map tableMappings, 97 | boolean prettyPrint) { 98 | assertThat(Translator.with( 99 | TranslatorConfig.builder().withPrettyPrint(prettyPrint).withTableToLabelMappings(tableMappings).build()) 100 | .convert(sql)).isEqualTo(expected); 101 | } 102 | 103 | private static class TestDataExtractor extends Treeprocessor { 104 | 105 | private final List testData = new ArrayList<>(); 106 | 107 | TestDataExtractor() { 108 | super(new HashMap<>()); // Must be mutable 109 | } 110 | 111 | @Override 112 | public Document process(Document document) { 113 | 114 | var blocks = document.findBy(Map.of("context", ":listing", "style", "source")).stream() 115 | .map(Block.class::cast).filter((b) -> b.hasAttribute("id")) 116 | .collect(Collectors.toMap(ContentNode::getId, Function.identity())); 117 | 118 | blocks.values().stream().filter((b) -> "sql".equals(b.getAttribute("language"))).map((sqlBlock) -> { 119 | var name = (String) sqlBlock.getAttribute("name"); 120 | var sql = String.join("\n", sqlBlock.getLines()); 121 | var cypherBlock = blocks.get(sqlBlock.getId() + "_expected"); 122 | var cypher = String.join("\n", cypherBlock.getLines()); 123 | Map tableMappings = new HashMap<>(); 124 | if (sqlBlock.getAttribute("table_mappings") != null) { 125 | tableMappings = Arrays.stream(((String) sqlBlock.getAttribute("table_mappings")).split(";")) 126 | .map(String::trim).map((s) -> s.split(":")) 127 | .collect(Collectors.toMap((a) -> a[0], (a) -> a[1])); 128 | } 129 | boolean parseCypher = Boolean.parseBoolean(((String) cypherBlock.getAttribute("parseCypher", "true"))); 130 | boolean prettyPrint = true; 131 | if (parseCypher) { 132 | cypher = CypherParser.parse(cypher).getCypher(); 133 | prettyPrint = false; 134 | } 135 | return new TestData(name, sql, cypher, tableMappings, prettyPrint); 136 | }).forEach(this.testData::add); 137 | return document; 138 | } 139 | 140 | } 141 | 142 | private record TestData(String name, String sql, String cypher, Map tableMappings, 143 | boolean prettyPrint) { 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /src/test/resources/dml.adoc: -------------------------------------------------------------------------------- 1 | :toc: 2 | 3 | = Tests for DML statements 4 | 5 | These tests cover SQL DML statements and their Cypher equivalents 6 | 7 | == Plain `DELETE` 8 | 9 | The input 10 | 11 | [source,sql,id=t0_0,name=delete] 12 | ---- 13 | DELETE FROM person 14 | ---- 15 | 16 | will be transpiled to 17 | 18 | [source,cypher,id=t0_0_expected] 19 | ---- 20 | MATCH (person:person) 21 | DELETE person 22 | ---- 23 | 24 | == `DELETE .. WHERE` 25 | 26 | The input 27 | 28 | [source,sql,id=t0_1,name=delete] 29 | ---- 30 | DELETE FROM person 31 | WHERE person.id = 1 32 | ---- 33 | 34 | will be transpiled to 35 | 36 | [source,cypher,id=t0_1_expected] 37 | ---- 38 | MATCH (person:person) 39 | WHERE person.id = 1 40 | DELETE person 41 | ---- 42 | 43 | == Plain `DELETE` with alias 44 | 45 | The input 46 | 47 | [source,sql,id=t0_2,name=delete] 48 | ---- 49 | DELETE FROM person p 50 | ---- 51 | 52 | will be transpiled to 53 | 54 | [source,cypher,id=t0_2_expected] 55 | ---- 56 | MATCH (p:person) 57 | DELETE p 58 | ---- 59 | 60 | == Plain `DELETE` with alias and table mapping 61 | 62 | The input 63 | 64 | [source,sql,id=t0_3,name=delete,table_mappings=person:Person] 65 | ---- 66 | DELETE FROM person p 67 | ---- 68 | 69 | will be transpiled to 70 | 71 | [source,cypher,id=t0_3_expected] 72 | ---- 73 | MATCH (p:Person) 74 | DELETE p 75 | ---- 76 | 77 | == Truncate 78 | 79 | The input 80 | 81 | [source,sql,id=t0_4,name=truncate,table_mappings=people:Person] 82 | ---- 83 | TRUNCATE TABLE people 84 | ---- 85 | 86 | will be transpiled to 87 | 88 | [source,cypher,id=t0_4_expected] 89 | ---- 90 | MATCH (people:Person) 91 | DETACH DELETE people 92 | ---- 93 | 94 | 95 | == Inserting data 96 | 97 | A single list of values with explicit columns and constant values 98 | 99 | [source,sql,id=t1_0,name=insert,table_mappings=people:Person] 100 | ---- 101 | INSERT INTO People (first_name, last_name, born) VALUES ('Helge', 'Schneider', 1955) 102 | ---- 103 | 104 | will be transpiled to 105 | 106 | [source,cypher,id=t1_0_expected] 107 | ---- 108 | CREATE (people:`Person` {first_name: 'Helge', last_name: 'Schneider', born: 1955}) 109 | ---- 110 | 111 | A single list of values with explicit columns and arguments 112 | 113 | [source,sql,id=t1_1,name=insert,table_mappings=people:Person] 114 | ---- 115 | INSERT INTO People (first_name, last_name, born) VALUES (?, ?, ?) 116 | ---- 117 | 118 | will be transpiled to 119 | 120 | [source,cypher,id=t1_1_expected] 121 | ---- 122 | CREATE (people:`Person` {first_name: $0, last_name: $1, born: $2}) 123 | ---- 124 | 125 | A single list of values with implicit columns and constant values 126 | 127 | [source,sql,id=t1_2,name=insert,table_mappings=people:Person] 128 | ---- 129 | INSERT INTO People VALUES ('Helge', 'Schneider', 1955) 130 | ---- 131 | 132 | will be transpiled to 133 | 134 | [source,cypher,id=t1_2_expected] 135 | ---- 136 | CREATE (people:`Person` {`unknown field 0`: 'Helge', `unknown field 1`: 'Schneider', `unknown field 2`: 1955}) 137 | ---- 138 | 139 | Multiple list of values with explicit columns and constant values 140 | 141 | [source,sql,id=t1_3,name=insert,table_mappings=people:Person] 142 | ---- 143 | INSERT INTO People (first_name, last_name, born) VALUES 144 | ('Helge', 'Schneider', 1955), 145 | ('Bela', 'B', 1962) 146 | ---- 147 | 148 | will be transpiled to 149 | 150 | [source,cypher,id=t1_3_expected] 151 | ---- 152 | UNWIND [ 153 | {first_name: 'Helge', last_name: 'Schneider', born: 1955}, 154 | {first_name: 'Bela', last_name: 'B', born: 1962}] 155 | AS properties 156 | CREATE (people:`Person`) 157 | SET people = properties 158 | ---- 159 | -------------------------------------------------------------------------------- /src/test/resources/expressions.adoc: -------------------------------------------------------------------------------- 1 | :toc: 2 | 3 | = Tests for column expressions 4 | 5 | These tests cover column expressions. 6 | 7 | == Literal Values 8 | 9 | The input 10 | 11 | [source,sql,id=t0_0,name=select_literal_values] 12 | ---- 13 | SELECT 14 | 1, TRUE, FALSE, NULL 15 | ---- 16 | 17 | will be transpiled to 18 | 19 | [source,cypher,id=t0_0_expected] 20 | ---- 21 | RETURN 1, TRUE, FALSE, NULL 22 | ---- 23 | 24 | == Arithmetic expressions 25 | 26 | The input 27 | 28 | [source,sql,id=t1_0,name=select_with_arithmetic] 29 | ---- 30 | SELECT 31 | 1 + 2, 32 | 1 - 2, 33 | 1 * 2, 34 | 1 / 2, 35 | square(2) 36 | ---- 37 | 38 | will be transpiled to 39 | 40 | [source,cypher,id=t1_0_expected] 41 | ---- 42 | RETURN 43 | (1 + 2), 44 | (1 - 2), 45 | (1 * 2), 46 | (1 / 2), 47 | (2 * 2) 48 | ---- 49 | 50 | == Numeric functions 51 | 52 | See https://neo4j.com/docs/cypher-manual/current/functions/mathematical-numeric/ 53 | 54 | The input 55 | 56 | [source,sql,id=t2_0,name=select_with_mathematical_functions] 57 | ---- 58 | SELECT 59 | abs(1), 60 | ceil(1), 61 | floor(1), 62 | round(1), 63 | round(1, 1), 64 | sign(1) 65 | ---- 66 | 67 | will be transpiled to 68 | 69 | [source,cypher,id=t2_0_expected] 70 | ---- 71 | RETURN 72 | abs(1), 73 | ceil(1), 74 | floor(1), 75 | round(1), 76 | round(1, 1), 77 | sign(1) 78 | ---- 79 | 80 | == Logarithmic functions 81 | 82 | See https://neo4j.com/docs/cypher-manual/current/functions/mathematical-logarithmic/ 83 | 84 | The input 85 | 86 | [source,sql,id=t3_0,name=select_with_logarithmic_functions] 87 | ---- 88 | SELECT 89 | exp(1), 90 | ln(1), 91 | log(2, 1), 92 | log10(1), 93 | sqrt(1) 94 | ---- 95 | 96 | will be transpiled to 97 | 98 | [source,cypher,id=t3_0_expected] 99 | ---- 100 | RETURN 101 | exp(1), 102 | log(1), 103 | (log(1) / log(2)), 104 | log10(1), 105 | sqrt(1) 106 | ---- 107 | 108 | 109 | == Trigonometric functions 110 | 111 | See https://neo4j.com/docs/cypher-manual/current/functions/mathematical-trigonometric/ 112 | 113 | The input 114 | 115 | [source,sql,id=t4_0,name=select_with_trigonometric_functions] 116 | ---- 117 | SELECT 118 | acos(1), 119 | asin(1), 120 | atan(1), 121 | atan2(1, 2), 122 | cos(1), 123 | cot(1), 124 | degrees(1), 125 | pi(), 126 | radians(1), 127 | sin(1), 128 | tan(1) 129 | ---- 130 | 131 | will be transpiled to 132 | 133 | [source,cypher,id=t4_0_expected] 134 | ---- 135 | RETURN 136 | acos(1), 137 | asin(1), 138 | atan(1), 139 | atan2(1, 2), 140 | cos(1), 141 | cot(1), 142 | degrees(1), 143 | pi(), 144 | radians(1), 145 | sin(1), 146 | tan(1) 147 | ---- 148 | 149 | 150 | 151 | == String functions 152 | 153 | See https://neo4j.com/docs/cypher-manual/current/functions/string/ 154 | 155 | The input 156 | 157 | [source,sql,id=t5_0,name=select_with_string_functions] 158 | ---- 159 | SELECT 160 | lower('abc'), 161 | cast(3 as varchar), 162 | trim(' abc '), 163 | length('abc'), 164 | left('abc', 2), 165 | ltrim(' abc '), 166 | replace('abc', 'b'), 167 | replace('abc', 'b', 'x'), 168 | reverse('abc'), 169 | right('abc', 2), 170 | rtrim(' abc '), 171 | substring('abc', 2 - 1), 172 | substring('abc', 2 - 1, 2), 173 | upper('abc') 174 | ---- 175 | 176 | will be transpiled to 177 | 178 | [source,cypher,id=t5_0_expected] 179 | ---- 180 | RETURN 181 | toLower('abc'), 182 | toString(3), 183 | trim(' abc '), 184 | size('abc'), 185 | left('abc', 2), 186 | ltrim(' abc '), 187 | replace('abc', 'b', NULL), 188 | replace('abc', 'b', 'x'), 189 | reverse('abc'), 190 | right('abc', 2), 191 | rtrim(' abc '), 192 | substring('abc', (2 - 1)), 193 | substring('abc', (2 - 1), 2), 194 | toUpper('abc') 195 | ---- 196 | 197 | 198 | == Scalar functions 199 | 200 | See https://neo4j.com/docs/cypher-manual/current/functions/scalar/ 201 | 202 | The input 203 | 204 | [source,sql,id=t6_0,name=select_with_string_functions] 205 | ---- 206 | SELECT 207 | coalesce(1, 2), 208 | coalesce(1, 2, 3), 209 | nvl(1, 2), 210 | cast('1' as boolean), 211 | cast(1 as float), 212 | cast(1 as double precision), 213 | cast(1 as real), 214 | cast(1 as tinyint), 215 | cast(1 as smallint), 216 | cast(1 as int), 217 | cast(1 as bigint) 218 | ---- 219 | 220 | will be transpiled to 221 | 222 | [source,cypher,id=t6_0_expected] 223 | ---- 224 | RETURN 225 | coalesce(1, 2), 226 | coalesce(1, 2, 3), 227 | coalesce(1, 2), 228 | toBoolean('1'), 229 | toFloat(1), 230 | toFloat(1), 231 | toFloat(1), 232 | toInteger(1), 233 | toInteger(1), 234 | toInteger(1), 235 | toInteger(1) 236 | ---- 237 | 238 | 239 | == Query syntax expressions 240 | 241 | See https://neo4j.com/docs/cypher-manual/current/syntax/expressions 242 | 243 | === `CASE` simple 244 | 245 | The input 246 | 247 | [source,sql,id=t7_0,name=select_with_string_functions] 248 | ---- 249 | SELECT 250 | CASE 1 WHEN 2 THEN 3 END, 251 | CASE 1 WHEN 2 THEN 3 ELSE 4 END, 252 | CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 END, 253 | CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 ELSE 6 END 254 | ---- 255 | 256 | will be transpiled to 257 | 258 | [source,cypher,id=t7_0_expected] 259 | ---- 260 | RETURN CASE 1 WHEN 2 THEN 3 END, CASE 1 WHEN 2 THEN 3 ELSE 4 END, CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 END, CASE 1 WHEN 2 THEN 3 WHEN 4 THEN 5 ELSE 6 END 261 | ---- 262 | 263 | === `CASE` searched 264 | 265 | The input 266 | 267 | [source,sql,id=t7_1,name=select_with_string_functions] 268 | ---- 269 | SELECT 270 | CASE WHEN 1 = 2 THEN 3 END, 271 | CASE WHEN 1 = 2 THEN 3 ELSE 4 END, 272 | CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 END, 273 | CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 ELSE 7 END 274 | ---- 275 | 276 | will be transpiled to 277 | 278 | [source,cypher,id=t7_1_expected] 279 | ---- 280 | RETURN 281 | CASE WHEN 1 = 2 THEN 3 END, 282 | CASE WHEN 1 = 2 THEN 3 ELSE 4 END, 283 | CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 END, 284 | CASE WHEN 1 = 2 THEN 3 WHEN 4 = 5 THEN 6 ELSE 7 END 285 | ---- 286 | 287 | === `CASE` abbreviations (which aren't `COALESCE` or `NVL`) 288 | 289 | The input 290 | 291 | [source,sql,id=t7_2,name=select_with_string_functions] 292 | ---- 293 | SELECT 294 | nullif(1, 2), 295 | nvl2(1, 2, 3) 296 | ---- 297 | 298 | will be transpiled to 299 | 300 | [source,cypher,id=t7_2_expected] 301 | ---- 302 | RETURN 303 | CASE WHEN 1 = 2 THEN NULL ELSE 1 END, 304 | CASE WHEN 1 IS NOT NULL THEN 2 ELSE 3 END 305 | ---- 306 | -------------------------------------------------------------------------------- /src/test/resources/joins.adoc: -------------------------------------------------------------------------------- 1 | :toc: 2 | 3 | = Joins to represent relationships 4 | 5 | These tests cover `ON JOIN` syntax to represent relationships 6 | 7 | == Foreign Key Joins 8 | 9 | The input 10 | 11 | [source,sql,id=t1_0,name=foreign_key_join] 12 | ---- 13 | SELECT p.name, m.title 14 | FROM `Person` as p 15 | JOIN `Movie` as m ON (m.id = p.`DIRECTED`) 16 | ---- 17 | 18 | will be transpiled to 19 | 20 | [source,cypher,id=t1_0_expected] 21 | ---- 22 | MATCH (p:Person)-[:DIRECTED]->(m:Movie) 23 | RETURN p.name, m.title 24 | ---- 25 | 26 | == Foreign Key Joins with simple syntax 27 | 28 | Statements shall be translated when not using backticks etc., too. The input 29 | 30 | [source,sql,id=t1_1,name=foreign_key_join_simple,table_mappings=people:Person;movies:Movie] 31 | ---- 32 | SELECT p.name, m.title 33 | FROM people p 34 | JOIN movies m ON m.id = p.directed 35 | ---- 36 | 37 | will be transpiled to 38 | 39 | [source,cypher,id=t1_1_expected] 40 | ---- 41 | MATCH (p:Person)-[:DIRECTED]->(m:Movie) 42 | RETURN p.name, m.title 43 | ---- 44 | 45 | == Join Table Joins 46 | 47 | The name of the join table will be used as the relationship type. 48 | In the example below, table mappings are configured: 49 | 50 | [source,sql,id=t2_0,name=join_table_join,table_mappings=people:Person;movies:Movie;movie_actors:ACTED_IN] 51 | ---- 52 | SELECT p.name, m.title 53 | FROM people p 54 | JOIN movie_actors r ON r.person_id = p.id 55 | JOIN movies m ON m.id = r.person_id 56 | ---- 57 | 58 | as demonstrated by 59 | 60 | [source,cypher,id=t2_0_expected] 61 | ---- 62 | MATCH (p:Person)-[r:ACTED_IN]->(m:Movie) 63 | RETURN p.name, m.title 64 | ---- 65 | 66 | Multiple joins will result in a chain of relationships: 67 | 68 | [source,sql,id=t2_1,name=join_multiple_table_join,table_mappings=people:Person;movies:Movie;movie_actors:ACTED_IN;movie_directors:DIRECTED] 69 | ---- 70 | SELECT p.name AS actor, d.name AS director, m.title 71 | FROM people p 72 | JOIN movie_actors r ON r.person_id = p.id 73 | JOIN movies m ON m.id = r.person_id 74 | JOIN movie_directors r2 ON r2.movie_id = m.id 75 | JOIN people d ON r2.person_id = d.id 76 | ---- 77 | 78 | as demonstrated by 79 | 80 | [source,cypher,id=t2_1_expected] 81 | ---- 82 | MATCH (p:`Person`)-[r:`ACTED_IN`]->(m:`Movie`)<-[r2:`DIRECTED`]-(d:`Person`) 83 | RETURN p.name AS actor, d.name AS director, m.title 84 | ---- 85 | 86 | Notice how the direction of the `DIRECTED` relationship is defined by the order of the join columns. 87 | -------------------------------------------------------------------------------- /src/test/resources/predicates.adoc: -------------------------------------------------------------------------------- 1 | :toc: 2 | 3 | = Tests for logical predicates 4 | 5 | These tests cover logical expressions and predicates 6 | 7 | == Logical Expressions / Conditions 8 | 9 | The input 10 | 11 | [source,sql,id=t1_0,name=logic_operators] 12 | ---- 13 | SELECT 1 FROM p WHERE 1 = 1 AND 2 = 2 OR 3 = 3 14 | ---- 15 | 16 | will be transpiled to 17 | 18 | [source,cypher,id=t1_0_expected] 19 | ---- 20 | MATCH (p:p) 21 | WHERE ((1 = 1 22 | AND 2 = 2) 23 | OR 3 = 3) 24 | RETURN 1 25 | ---- 26 | 27 | The input 28 | 29 | [source,sql,id=t1_1,name=logic_operators_rare] 30 | ---- 31 | SELECT 1 FROM p WHERE NOT 1 = 1 XOR 2 = 2 32 | ---- 33 | 34 | will be transpiled to 35 | 36 | [source,cypher,id=t1_1_expected] 37 | ---- 38 | MATCH (p:p) 39 | WHERE (NOT (1 = 1) 40 | XOR 2 = 2) 41 | RETURN 1 42 | ---- 43 | 44 | 45 | == Logical Expressions / Conditions (row value expressions) 46 | 47 | The input 48 | 49 | [source,sql,id=t1_2,name=logic_operators] 50 | ---- 51 | SELECT 1 52 | FROM p 53 | WHERE (1, 2) = (3, 4) 54 | OR (1, 2) < (3, 4) 55 | OR (1, 2) <= (3, 4) 56 | OR (1, 2, 3) <> (4, 5, 6) 57 | OR (1, 2, 3) > (4, 5, 6) 58 | OR (1, 2, 3) >= (4, 5, 6) 59 | ---- 60 | 61 | will be transpiled to 62 | 63 | [source,cypher,id=t1_2_expected] 64 | ---- 65 | MATCH (p:p) 66 | WHERE 1 = 3 AND 2 = 4 67 | OR (1 < 3 OR 1 = 3 AND 2 < 4) 68 | OR (1 < 3 OR 1 = 3 AND 2 <= 4) 69 | OR (1 != 4 AND 2 != 5 AND 3 != 6) 70 | OR (1 > 4 OR 1 = 4 AND (2 > 5 OR 2 = 5 AND 3 > 6)) 71 | OR (1 > 4 OR 1 = 4 AND (2 > 5 OR 2 = 5 AND 3 >= 6)) 72 | RETURN 1 73 | ---- 74 | 75 | 76 | == Arithmetic Operators 77 | 78 | The input 79 | 80 | [source,sql,id=t2_0,name=predicates_with_arithmetics] 81 | ---- 82 | SELECT 1 FROM p WHERE 1 = 1 AND 2 > 1 AND 1 < 2 AND 1 <= 2 AND 2 >= 1 AND 1 != 2 83 | ---- 84 | 85 | will be transpiled to 86 | 87 | [source,cypher,id=t2_0_expected] 88 | ---- 89 | MATCH (p:p) 90 | WHERE (1 = 1 91 | AND 2 > 1 92 | AND 1 < 2 93 | AND 1 <= 2 94 | AND 2 >= 1 95 | AND 1 <> 2) 96 | RETURN 1 97 | ---- 98 | 99 | === Between 100 | 101 | Between in SQL is inclusive 102 | 103 | [source,sql,id=t2_1,name=predicate_between] 104 | ---- 105 | SELECT 1 FROM p WHERE 2 BETWEEN 1 AND 3 106 | ---- 107 | 108 | will be transpiled to. 109 | There is a shorter form in Cypher (`1 <= 2 <= 3`) but that's parsed into `AND` form. 110 | 111 | [source,cypher,id=t2_1_expected] 112 | ---- 113 | MATCH (p:p) 114 | WHERE (1 <= 2) AND (2 <= 3) 115 | RETURN 1 116 | ---- 117 | 118 | [source,sql,id=t2_2,name=predicate_between_symmetric] 119 | ---- 120 | SELECT 1 FROM p WHERE 2 BETWEEN SYMMETRIC 3 AND 1 121 | ---- 122 | 123 | [source,cypher,id=t2_2_expected] 124 | ---- 125 | MATCH (p:p) 126 | WHERE (3 <= 2) AND (2 <= 1) OR (1 <= 2) AND (2 <= 3) 127 | RETURN 1 128 | ---- 129 | 130 | 131 | == Null Handling 132 | 133 | The input 134 | 135 | [source,sql,id=t3_0,name=predicates_nullability] 136 | ---- 137 | SELECT 1 FROM p WHERE 1 IS NULL AND 2 IS NOT NULL 138 | ---- 139 | 140 | will be transpiled to 141 | 142 | [source,cypher,id=t3_0_expected] 143 | ---- 144 | MATCH (p:p) 145 | WHERE (1 IS NULL 146 | AND 2 IS NOT NULL) 147 | RETURN 1 148 | ---- 149 | 150 | 151 | == Null handling (row value expressions) 152 | 153 | The input 154 | 155 | [source,sql,id=t4_0,name=predicates_row_is_null] 156 | ---- 157 | SELECT 1 FROM p WHERE (1, 2) IS NULL OR (3, 4) IS NOT NULL 158 | ---- 159 | 160 | will be transpiled to 161 | 162 | [source,cypher,id=t4_0_expected] 163 | ---- 164 | MATCH (p:p) 165 | WHERE 166 | (1 IS NULL AND 2 IS NULL) 167 | OR (3 IS NOT NULL AND 4 IS NOT NULL) 168 | RETURN 1 169 | ---- 170 | 171 | == `LIKE` can be used 172 | 173 | The `LIKE` comparator can be used, too: 174 | 175 | [source,sql,id=t5_0,name=predicates_like] 176 | ---- 177 | SELECT * FROM movies m WHERE m.title LIKE '%Matrix%' 178 | ---- 179 | 180 | will be transpiled to 181 | 182 | [source,cypher,id=t5_0_expected] 183 | ---- 184 | MATCH (m:`movies`) WHERE m.title =~ '.*Matrix.*' 185 | RETURN * 186 | ---- 187 | -------------------------------------------------------------------------------- /src/test/resources/simple.adoc: -------------------------------------------------------------------------------- 1 | :toc: 2 | 3 | = Tests for simple select queries 4 | 5 | These tests cover single table selects and basic conditions. 6 | 7 | == PoC 8 | 9 | The input 10 | 11 | [source,sql,id=t1_0,name=select_with_condition] 12 | ---- 13 | SELECT t.a, t.b 14 | FROM `MY_TABLE` AS t 15 | WHERE t.a = 1 16 | ---- 17 | 18 | will be transpiled to 19 | 20 | [source,cypher,id=t1_0_expected] 21 | ---- 22 | MATCH (t:MY_TABLE) 23 | WHERE t.a = 1 24 | RETURN t.a, t.b 25 | ---- 26 | 27 | [source,sql,id=t1_1,name=no_driving_table] 28 | ---- 29 | SELECT 1 30 | ---- 31 | 32 | [source,cypher,id=t1_1_expected] 33 | ---- 34 | RETURN 1 35 | ---- 36 | 37 | == Comparing SQL with Cypher examples 38 | 39 | Sources of the following examples are from https://neo4j.com/developer/cypher/guide-sql-to-cypher/[Comparing SQL with Cypher]. 40 | 41 | === Find all Products 42 | 43 | ==== Select and Return Records 44 | 45 | Easy in SQL, just select everything from the `products` table. 46 | 47 | [source,sql,id=t2_0,name=select_and_return_records,table_mappings=products:Product] 48 | ---- 49 | SELECT p.* 50 | FROM products as p 51 | ---- 52 | 53 | Similarly, in Cypher, you just *match* a simple pattern: all nodes with the *label* `:Product` and `RETURN` them. 54 | 55 | [source,cypher,id=t2_0_expected] 56 | ---- 57 | MATCH (p:Product) 58 | RETURN p 59 | ---- 60 | 61 | ==== Field Access, Ordering and Paging 62 | 63 | *More efficient is to return only a subset of attributes*, like `ProductName` and `UnitPrice`. 64 | And while we're on it, let's also order by price and only return the 10 most expensive items. 65 | 66 | [source,sql,id=t2_1,name=field_acces_ordering_paging,table_mappings=products:Product] 67 | ---- 68 | SELECT p.`productName`, p.`unitPrice` 69 | FROM products as p 70 | ORDER BY p.`unitPrice` DESC 71 | LIMIT 10 72 | ---- 73 | 74 | You can copy and paste the changes from SQL to Cypher, it's thankfully unsurprising. 75 | But remember that labels, relationship-types and property-names are *case sensitive* in Neo4j. 76 | 77 | [source,cypher,id=t2_1_expected] 78 | ---- 79 | MATCH (p:Product) 80 | RETURN p.productName, p.unitPrice ORDER BY p.unitPrice DESC LIMIT 10 81 | ---- 82 | --------------------------------------------------------------------------------