diff --git a/LICENSES.txt b/LICENSES.txt index 7e33fd625..f80e970d6 100644 --- a/LICENSES.txt +++ b/LICENSES.txt @@ -9,34 +9,34 @@ Apache-2.0 RoaringBitmap-0.7.17.jar WMI4Java-1.6.3.jar accessors-smart-2.5.0.jar - aircompressor-0.27.jar + aircompressor-2.0.2.jar annotations-17.0.0.jar annotations-4.1.1.4.jar - arrow-format-18.0.0.jar - arrow-memory-core-18.0.0.jar - arrow-memory-netty-18.0.0.jar - arrow-memory-netty-buffer-patch-18.0.0.jar - arrow-vector-18.0.0.jar - assertj-core-3.26.3.jar + arrow-format-18.1.0.jar + arrow-memory-core-18.1.0.jar + arrow-memory-netty-18.1.0.jar + arrow-memory-netty-buffer-patch-18.1.0.jar + arrow-vector-18.1.0.jar + assertj-core-3.27.2.jar audience-annotations-0.12.0.jar avro-1.9.2.jar awaitility-4.2.2.jar aws-java-sdk-core-1.12.643.jar aws-java-sdk-kms-1.12.643.jar aws-java-sdk-s3-1.12.643.jar - byte-buddy-1.15.4.jar - byte-buddy-agent-1.15.4.jar + byte-buddy-1.15.11.jar + byte-buddy-agent-1.15.11.jar caffeine-3.1.8.jar cassandra-driver-core-3.10.0.jar commons-beanutils-1.9.4.jar commons-cli-1.5.0.jar - commons-codec-1.17.1.jar + commons-codec-1.17.2.jar commons-collections-3.2.2.jar commons-compress-1.27.1.jar commons-configuration2-2.11.0.jar commons-csv-1.9.0.jar commons-daemon-1.0.13.jar - commons-io-2.17.0.jar + commons-io-2.18.0.jar commons-lang-2.6.jar commons-lang3-3.12.0.jar commons-lang3-3.17.0.jar @@ -44,7 +44,7 @@ Apache-2.0 commons-math3-3.6.1.jar commons-net-3.9.0.jar commons-text-1.10.0.jar - commons-text-1.12.0.jar + commons-text-1.13.0.jar curator-client-5.2.0.jar curator-framework-5.2.0.jar curator-recipes-5.2.0.jar @@ -52,21 +52,21 @@ Apache-2.0 docker-java-transport-3.4.0.jar docker-java-transport-zerodep-3.4.0.jar ehcache-3.3.1.jar - error_prone_annotations-2.28.0.jar + error_prone_annotations-2.30.0.jar failureaccess-1.0.2.jar flatbuffers-java-24.3.25.jar - flight-core-18.0.0.jar + flight-core-18.1.0.jar fst-2.50.jar geronimo-jcache_1.0_spec-1.0-alpha-1.jar gradle-tooling-api-7.3.jar - grpc-api-1.68.1.jar - grpc-context-1.68.1.jar - grpc-core-1.68.1.jar - grpc-netty-1.68.1.jar - grpc-protobuf-1.68.1.jar - grpc-protobuf-lite-1.68.1.jar - grpc-stub-1.68.1.jar - grpc-util-1.68.1.jar + grpc-api-1.69.0.jar + grpc-context-1.69.0.jar + grpc-core-1.69.0.jar + grpc-netty-1.69.0.jar + grpc-protobuf-1.69.0.jar + grpc-protobuf-lite-1.69.0.jar + grpc-stub-1.69.0.jar + grpc-util-1.69.0.jar gson-2.11.0.jar guava-33.3.1-jre.jar guice-4.2.3.jar @@ -99,25 +99,21 @@ Apache-2.0 hadoop-yarn-server-tests-3.4.0-tests.jar hadoop-yarn-server-timelineservice-3.4.0.jar hadoop-yarn-server-web-proxy-3.4.0.jar - http2-common-10.0.24.jar - http2-hpack-10.0.24.jar - http2-server-10.0.24.jar httpclient-4.5.13.jar httpcore-4.4.13.jar ipaddress-5.5.1.jar j2objc-annotations-3.0.0.jar jPowerShell-3.0.jar jProcesses-1.6.5.jar - jackson-annotations-2.18.1.jar - jackson-core-2.18.1.jar - jackson-databind-2.18.1.jar - jackson-dataformat-cbor-2.18.1.jar - jackson-dataformat-csv-2.18.1.jar - jackson-datatype-jdk8-2.18.1.jar - jackson-datatype-jsr310-2.18.1.jar - jackson-jaxrs-base-2.18.1.jar - jackson-jaxrs-json-provider-2.18.1.jar - jackson-module-jaxb-annotations-2.18.1.jar + jackson-annotations-2.18.2.jar + jackson-core-2.18.2.jar + jackson-databind-2.18.2.jar + jackson-dataformat-cbor-2.18.2.jar + jackson-dataformat-csv-2.18.2.jar + jackson-datatype-jsr310-2.18.2.jar + jackson-jaxrs-base-2.18.2.jar + jackson-jaxrs-json-provider-2.18.2.jar + jackson-module-jaxb-annotations-2.18.2.jar jakarta.validation-api-2.0.2.jar javapoet-1.13.0.jar javassist-3.30.2-GA.jar @@ -127,21 +123,28 @@ Apache-2.0 jcip-annotations-1.0-1.jar jctools-core-4.0.5.jar jettison-1.5.4.jar - jetty-alpn-java-server-10.0.24.jar - jetty-alpn-server-10.0.24.jar - jetty-http-10.0.24.jar - jetty-io-10.0.24.jar - jetty-security-10.0.24.jar - jetty-server-10.0.24.jar - jetty-servlet-10.0.24.jar + jetty-alpn-java-server-12.0.16.jar + jetty-alpn-server-12.0.16.jar + jetty-ee-12.0.16.jar + jetty-ee8-nested-12.0.16.jar + jetty-ee8-security-12.0.16.jar + jetty-ee8-servlet-12.0.16.jar + jetty-ee8-webapp-12.0.16.jar + jetty-http-12.0.16.jar + jetty-http2-common-12.0.16.jar + jetty-http2-hpack-12.0.16.jar + jetty-http2-server-12.0.16.jar + jetty-io-12.0.16.jar + jetty-security-12.0.16.jar + jetty-server-12.0.16.jar jetty-servlet-api-4.0.6.jar - jetty-util-10.0.24.jar - jetty-webapp-10.0.24.jar - jetty-xml-10.0.24.jar + jetty-session-12.0.16.jar + jetty-util-12.0.16.jar + jetty-xml-12.0.16.jar jffi-1.2.16-native.jar jffi-1.2.16.jar jmespath-java-1.12.643.jar - jna-5.15.0.jar + jna-5.16.0.jar jnr-constants-0.9.9.jar jnr-ffi-2.1.7.jar joda-time-2.8.1.jar @@ -213,32 +216,32 @@ Apache-2.0 objenesis-3.3.jar opencsv-5.7.1.jar opentest4j-1.3.0.jar - parquet-column-1.14.3.jar - parquet-common-1.14.3.jar - parquet-encoding-1.14.3.jar - parquet-floor-1.47.jar - parquet-format-structures-1.14.3.jar - parquet-hadoop-1.14.3.jar - parquet-jackson-1.14.3.jar + parquet-column-1.15.0.jar + parquet-common-1.15.0.jar + parquet-encoding-1.15.0.jar + parquet-floor-1.49.jar + parquet-format-structures-1.15.0.jar + parquet-hadoop-1.15.0.jar + parquet-jackson-1.15.0.jar perfmark-api-0.27.0.jar picocli-4.7.6.jar - proto-google-common-protos-2.41.0.jar - reactor-core-3.6.12.jar + proto-google-common-protos-2.48.0.jar + reactor-core-3.6.13.jar reload4j-1.2.22.jar scala-collection-contrib_2.13-0.3.0.jar scala-library-2.13.11.jar scala-reflect-2.13.11.jar - shiro-cache-2.0.1.jar - shiro-config-core-2.0.1.jar - shiro-core-2.0.1.jar - shiro-crypto-cipher-2.0.1.jar - shiro-crypto-core-2.0.1.jar - shiro-crypto-hash-2.0.1.jar - shiro-event-2.0.1.jar - shiro-hashes-argon2-2.0.1.jar - shiro-hashes-bcrypt-2.0.1.jar - shiro-lang-2.0.1.jar - snappy-java-1.1.10.5.jar + shiro-cache-2.0.2.jar + shiro-config-core-2.0.2.jar + shiro-core-2.0.2.jar + shiro-crypto-cipher-2.0.2.jar + shiro-crypto-core-2.0.2.jar + shiro-crypto-hash-2.0.2.jar + shiro-event-2.0.2.jar + shiro-hashes-argon2-2.0.2.jar + shiro-hashes-bcrypt-2.0.2.jar + shiro-lang-2.0.2.jar + snappy-java-1.1.10.7.jar token-provider-2.0.3.jar websocket-api-9.4.53.v20231009.jar websocket-client-9.4.53.v20231009.jar @@ -457,7 +460,7 @@ BSD-2-Clause jline-3.22.0.jar jsch-0.1.55.jar stax2-api-4.2.1.jar - zstd-jni-1.5.6-7.jar + zstd-jni-1.5.6-9.jar ------------------------------------------------------------------------------ Copyright @@ -2206,14 +2209,14 @@ Eclipse Public License - v 2.0 jersey-container-servlet-core-2.43.jar jersey-hk2-2.43.jar jersey-server-2.43.jar - junit-jupiter-5.11.3.jar - junit-jupiter-api-5.11.3.jar - junit-jupiter-engine-5.11.3.jar - junit-jupiter-params-5.11.3.jar - junit-platform-commons-1.11.3.jar - junit-platform-engine-1.11.3.jar - junit-platform-launcher-1.11.3.jar - junit-platform-testkit-1.11.3.jar + junit-jupiter-5.11.4.jar + junit-jupiter-api-5.11.4.jar + junit-jupiter-engine-5.11.4.jar + junit-jupiter-params-5.11.4.jar + junit-platform-commons-1.11.4.jar + junit-platform-engine-1.11.4.jar + junit-platform-launcher-1.11.4.jar + junit-platform-testkit-1.11.4.jar osgi-resource-locator-1.0.3.jar ------------------------------------------------------------------------------ @@ -2912,7 +2915,7 @@ MIT jersey-hk2-2.43.jar jnr-x86asm-1.0.2.jar localstack-1.20.2.jar - mockito-core-5.14.2.jar + mockito-core-5.15.2.jar mssql-jdbc-6.2.1.jre7.jar mysql-1.20.2.jar neo4j-1.20.2.jar diff --git a/NOTICE.txt b/NOTICE.txt index 4338e9585..8c4f81c91 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -39,34 +39,34 @@ Apache-2.0 RoaringBitmap-0.7.17.jar WMI4Java-1.6.3.jar accessors-smart-2.5.0.jar - aircompressor-0.27.jar + aircompressor-2.0.2.jar annotations-17.0.0.jar annotations-4.1.1.4.jar - arrow-format-18.0.0.jar - arrow-memory-core-18.0.0.jar - arrow-memory-netty-18.0.0.jar - arrow-memory-netty-buffer-patch-18.0.0.jar - arrow-vector-18.0.0.jar - assertj-core-3.26.3.jar + arrow-format-18.1.0.jar + arrow-memory-core-18.1.0.jar + arrow-memory-netty-18.1.0.jar + arrow-memory-netty-buffer-patch-18.1.0.jar + arrow-vector-18.1.0.jar + assertj-core-3.27.2.jar audience-annotations-0.12.0.jar avro-1.9.2.jar awaitility-4.2.2.jar aws-java-sdk-core-1.12.643.jar aws-java-sdk-kms-1.12.643.jar aws-java-sdk-s3-1.12.643.jar - byte-buddy-1.15.4.jar - byte-buddy-agent-1.15.4.jar + byte-buddy-1.15.11.jar + byte-buddy-agent-1.15.11.jar caffeine-3.1.8.jar cassandra-driver-core-3.10.0.jar commons-beanutils-1.9.4.jar commons-cli-1.5.0.jar - commons-codec-1.17.1.jar + commons-codec-1.17.2.jar commons-collections-3.2.2.jar commons-compress-1.27.1.jar commons-configuration2-2.11.0.jar commons-csv-1.9.0.jar commons-daemon-1.0.13.jar - commons-io-2.17.0.jar + commons-io-2.18.0.jar commons-lang-2.6.jar commons-lang3-3.12.0.jar commons-lang3-3.17.0.jar @@ -74,7 +74,7 @@ Apache-2.0 commons-math3-3.6.1.jar commons-net-3.9.0.jar commons-text-1.10.0.jar - commons-text-1.12.0.jar + commons-text-1.13.0.jar curator-client-5.2.0.jar curator-framework-5.2.0.jar curator-recipes-5.2.0.jar @@ -82,21 +82,21 @@ Apache-2.0 docker-java-transport-3.4.0.jar docker-java-transport-zerodep-3.4.0.jar ehcache-3.3.1.jar - error_prone_annotations-2.28.0.jar + error_prone_annotations-2.30.0.jar failureaccess-1.0.2.jar flatbuffers-java-24.3.25.jar - flight-core-18.0.0.jar + flight-core-18.1.0.jar fst-2.50.jar geronimo-jcache_1.0_spec-1.0-alpha-1.jar gradle-tooling-api-7.3.jar - grpc-api-1.68.1.jar - grpc-context-1.68.1.jar - grpc-core-1.68.1.jar - grpc-netty-1.68.1.jar - grpc-protobuf-1.68.1.jar - grpc-protobuf-lite-1.68.1.jar - grpc-stub-1.68.1.jar - grpc-util-1.68.1.jar + grpc-api-1.69.0.jar + grpc-context-1.69.0.jar + grpc-core-1.69.0.jar + grpc-netty-1.69.0.jar + grpc-protobuf-1.69.0.jar + grpc-protobuf-lite-1.69.0.jar + grpc-stub-1.69.0.jar + grpc-util-1.69.0.jar gson-2.11.0.jar guava-33.3.1-jre.jar guice-4.2.3.jar @@ -129,25 +129,21 @@ Apache-2.0 hadoop-yarn-server-tests-3.4.0-tests.jar hadoop-yarn-server-timelineservice-3.4.0.jar hadoop-yarn-server-web-proxy-3.4.0.jar - http2-common-10.0.24.jar - http2-hpack-10.0.24.jar - http2-server-10.0.24.jar httpclient-4.5.13.jar httpcore-4.4.13.jar ipaddress-5.5.1.jar j2objc-annotations-3.0.0.jar jPowerShell-3.0.jar jProcesses-1.6.5.jar - jackson-annotations-2.18.1.jar - jackson-core-2.18.1.jar - jackson-databind-2.18.1.jar - jackson-dataformat-cbor-2.18.1.jar - jackson-dataformat-csv-2.18.1.jar - jackson-datatype-jdk8-2.18.1.jar - jackson-datatype-jsr310-2.18.1.jar - jackson-jaxrs-base-2.18.1.jar - jackson-jaxrs-json-provider-2.18.1.jar - jackson-module-jaxb-annotations-2.18.1.jar + jackson-annotations-2.18.2.jar + jackson-core-2.18.2.jar + jackson-databind-2.18.2.jar + jackson-dataformat-cbor-2.18.2.jar + jackson-dataformat-csv-2.18.2.jar + jackson-datatype-jsr310-2.18.2.jar + jackson-jaxrs-base-2.18.2.jar + jackson-jaxrs-json-provider-2.18.2.jar + jackson-module-jaxb-annotations-2.18.2.jar jakarta.validation-api-2.0.2.jar javapoet-1.13.0.jar javassist-3.30.2-GA.jar @@ -157,21 +153,28 @@ Apache-2.0 jcip-annotations-1.0-1.jar jctools-core-4.0.5.jar jettison-1.5.4.jar - jetty-alpn-java-server-10.0.24.jar - jetty-alpn-server-10.0.24.jar - jetty-http-10.0.24.jar - jetty-io-10.0.24.jar - jetty-security-10.0.24.jar - jetty-server-10.0.24.jar - jetty-servlet-10.0.24.jar + jetty-alpn-java-server-12.0.16.jar + jetty-alpn-server-12.0.16.jar + jetty-ee-12.0.16.jar + jetty-ee8-nested-12.0.16.jar + jetty-ee8-security-12.0.16.jar + jetty-ee8-servlet-12.0.16.jar + jetty-ee8-webapp-12.0.16.jar + jetty-http-12.0.16.jar + jetty-http2-common-12.0.16.jar + jetty-http2-hpack-12.0.16.jar + jetty-http2-server-12.0.16.jar + jetty-io-12.0.16.jar + jetty-security-12.0.16.jar + jetty-server-12.0.16.jar jetty-servlet-api-4.0.6.jar - jetty-util-10.0.24.jar - jetty-webapp-10.0.24.jar - jetty-xml-10.0.24.jar + jetty-session-12.0.16.jar + jetty-util-12.0.16.jar + jetty-xml-12.0.16.jar jffi-1.2.16-native.jar jffi-1.2.16.jar jmespath-java-1.12.643.jar - jna-5.15.0.jar + jna-5.16.0.jar jnr-constants-0.9.9.jar jnr-ffi-2.1.7.jar joda-time-2.8.1.jar @@ -243,32 +246,32 @@ Apache-2.0 objenesis-3.3.jar opencsv-5.7.1.jar opentest4j-1.3.0.jar - parquet-column-1.14.3.jar - parquet-common-1.14.3.jar - parquet-encoding-1.14.3.jar - parquet-floor-1.47.jar - parquet-format-structures-1.14.3.jar - parquet-hadoop-1.14.3.jar - parquet-jackson-1.14.3.jar + parquet-column-1.15.0.jar + parquet-common-1.15.0.jar + parquet-encoding-1.15.0.jar + parquet-floor-1.49.jar + parquet-format-structures-1.15.0.jar + parquet-hadoop-1.15.0.jar + parquet-jackson-1.15.0.jar perfmark-api-0.27.0.jar picocli-4.7.6.jar - proto-google-common-protos-2.41.0.jar - reactor-core-3.6.12.jar + proto-google-common-protos-2.48.0.jar + reactor-core-3.6.13.jar reload4j-1.2.22.jar scala-collection-contrib_2.13-0.3.0.jar scala-library-2.13.11.jar scala-reflect-2.13.11.jar - shiro-cache-2.0.1.jar - shiro-config-core-2.0.1.jar - shiro-core-2.0.1.jar - shiro-crypto-cipher-2.0.1.jar - shiro-crypto-core-2.0.1.jar - shiro-crypto-hash-2.0.1.jar - shiro-event-2.0.1.jar - shiro-hashes-argon2-2.0.1.jar - shiro-hashes-bcrypt-2.0.1.jar - shiro-lang-2.0.1.jar - snappy-java-1.1.10.5.jar + shiro-cache-2.0.2.jar + shiro-config-core-2.0.2.jar + shiro-core-2.0.2.jar + shiro-crypto-cipher-2.0.2.jar + shiro-crypto-core-2.0.2.jar + shiro-crypto-hash-2.0.2.jar + shiro-event-2.0.2.jar + shiro-hashes-argon2-2.0.2.jar + shiro-hashes-bcrypt-2.0.2.jar + shiro-lang-2.0.2.jar + snappy-java-1.1.10.7.jar token-provider-2.0.3.jar websocket-api-9.4.53.v20231009.jar websocket-client-9.4.53.v20231009.jar @@ -289,7 +292,7 @@ BSD-2-Clause jline-3.22.0.jar jsch-0.1.55.jar stax2-api-4.2.1.jar - zstd-jni-1.5.6-7.jar + zstd-jni-1.5.6-9.jar BSD-2-Clause dnsjava-3.4.0.jar @@ -364,19 +367,23 @@ Eclipse Public License - Version 1.0 jetty-servlet-api-4.0.6.jar Eclipse Public License - Version 2.0 - http2-common-10.0.24.jar - http2-hpack-10.0.24.jar - http2-server-10.0.24.jar - jetty-alpn-java-server-10.0.24.jar - jetty-alpn-server-10.0.24.jar - jetty-http-10.0.24.jar - jetty-io-10.0.24.jar - jetty-security-10.0.24.jar - jetty-server-10.0.24.jar - jetty-servlet-10.0.24.jar - jetty-util-10.0.24.jar - jetty-webapp-10.0.24.jar - jetty-xml-10.0.24.jar + jetty-alpn-java-server-12.0.16.jar + jetty-alpn-server-12.0.16.jar + jetty-ee-12.0.16.jar + jetty-ee8-nested-12.0.16.jar + jetty-ee8-security-12.0.16.jar + jetty-ee8-servlet-12.0.16.jar + jetty-ee8-webapp-12.0.16.jar + jetty-http-12.0.16.jar + jetty-http2-common-12.0.16.jar + jetty-http2-hpack-12.0.16.jar + jetty-http2-server-12.0.16.jar + jetty-io-12.0.16.jar + jetty-security-12.0.16.jar + jetty-server-12.0.16.jar + jetty-session-12.0.16.jar + jetty-util-12.0.16.jar + jetty-xml-12.0.16.jar Eclipse Public License - v 1.0 eclipse-collections-11.1.0.jar @@ -400,14 +407,14 @@ Eclipse Public License - v 2.0 jersey-container-servlet-core-2.43.jar jersey-hk2-2.43.jar jersey-server-2.43.jar - junit-jupiter-5.11.3.jar - junit-jupiter-api-5.11.3.jar - junit-jupiter-engine-5.11.3.jar - junit-jupiter-params-5.11.3.jar - junit-platform-commons-1.11.3.jar - junit-platform-engine-1.11.3.jar - junit-platform-launcher-1.11.3.jar - junit-platform-testkit-1.11.3.jar + junit-jupiter-5.11.4.jar + junit-jupiter-api-5.11.4.jar + junit-jupiter-engine-5.11.4.jar + junit-jupiter-params-5.11.4.jar + junit-platform-commons-1.11.4.jar + junit-platform-engine-1.11.4.jar + junit-platform-launcher-1.11.4.jar + junit-platform-testkit-1.11.4.jar osgi-resource-locator-1.0.3.jar GNU General Public License (GPL), version 2, with the Classpath exception @@ -449,7 +456,7 @@ LGPL 2.1 javassist-3.30.2-GA.jar LGPL-2.1-or-later - jna-5.15.0.jar + jna-5.16.0.jar MIT bcpkix-jdk15on-1.70.jar @@ -471,7 +478,7 @@ MIT jersey-hk2-2.43.jar jnr-x86asm-1.0.2.jar localstack-1.20.2.jar - mockito-core-5.14.2.jar + mockito-core-5.15.2.jar mssql-jdbc-6.2.1.jre7.jar mysql-1.20.2.jar neo4j-1.20.2.jar diff --git a/common/src/main/java/apoc/cypher/CypherUtils.java b/common/src/main/java/apoc/cypher/CypherUtils.java index c0f914fde..955a6735f 100644 --- a/common/src/main/java/apoc/cypher/CypherUtils.java +++ b/common/src/main/java/apoc/cypher/CypherUtils.java @@ -23,19 +23,24 @@ import static java.util.stream.Collectors.toList; import apoc.result.CypherStatementMapResult; +import apoc.util.Util; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.stream.Stream; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.Name; public class CypherUtils { public static Stream runCypherQuery( - Transaction tx, @Name("cypher") String statement, @Name("params") Map params) { + Transaction tx, + @Name("cypher") String statement, + @Name("params") Map params, + ProcedureCallContext procedureCallContext) { if (params == null) params = Collections.emptyMap(); - return tx.execute(withParamMapping(statement, params.keySet()), params).stream() - .map(CypherStatementMapResult::new); + String query = Util.prefixQueryWithCheck(procedureCallContext, withParamMapping(statement, params.keySet())); + return tx.execute(query, params).stream().map(CypherStatementMapResult::new); } public static String withParamMapping(String fragment, Collection keys) { diff --git a/common/src/main/java/apoc/util/Util.java b/common/src/main/java/apoc/util/Util.java index 6175a2040..00eae82fb 100644 --- a/common/src/main/java/apoc/util/Util.java +++ b/common/src/main/java/apoc/util/Util.java @@ -110,6 +110,7 @@ import org.neo4j.graphdb.schema.IndexType; import org.neo4j.graphdb.security.URLAccessChecker; import org.neo4j.graphdb.security.URLAccessValidationError; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.internal.schema.ConstraintDescriptor; import org.neo4j.kernel.api.QueryLanguage; import org.neo4j.kernel.impl.coreapi.InternalTransaction; @@ -1363,6 +1364,118 @@ public static T withBackOffRetries(Supplier func, long initialTimeout, lo return result; } + public static String getCypherVersionString(ProcedureCallContext procedureCallContext) { + if (procedureCallContext.calledwithQueryLanguage().equals(QueryLanguage.CYPHER_5)) { + return "5"; + } else if (procedureCallContext.calledwithQueryLanguage().equals(QueryLanguage.CYPHER_25)) { + return "25"; + } else { + // When a new version of Cypher is added, please update here :) + throw new RuntimeException( + "Unsupported cypher query language: " + procedureCallContext.calledwithQueryLanguage()); + } + } + + private static final Pattern CYPHER_VERSION_PATTERN = + Pattern.compile("^\\s*\\b(CYPHER)(?:\\s+(\\d+))?", Pattern.CASE_INSENSITIVE); + public static final Pattern PLANNER_PATTERN = + Pattern.compile("\\bplanner\\s*=\\s*[^\\s]*", Pattern.CASE_INSENSITIVE); + public static final Pattern RUNTIME_PATTERN = Pattern.compile("\\bruntime\\s*=", Pattern.CASE_INSENSITIVE); + public static final String CYPHER_RUNTIME_SLOTTED = " runtime=slotted "; + + public static String prefixQueryWithCheck(ProcedureCallContext procedureCallContext, String query) { + return prefixQueryWithCheck(getCypherVersionString(procedureCallContext), query); + } + + /** + * A helper function to prefix a query with a Cypher Version; if it is not already prefixed. + * @param cypherVersion a string of the version number + * @return The prefixed query + */ + public static String prefixQueryWithCheck(String cypherVersion, String query) { + List cypherPrefix = extractCypherPrefix(query, cypherVersion); + if (Objects.equals(cypherPrefix.getFirst(), "")) { + return cypherPrefix.get(1) + " " + query; + } + return query.replaceFirst("(?i)" + cypherPrefix.getFirst(), cypherPrefix.get(1) + " "); + } + + // Extract the Cypher prefix, add version if missing + // This will return a list of 2 items, the first being what was extracted (empty string if nothing) + // The second being what the prefix should be. + public static List extractCypherPrefix(String input, String cypherVersion) { + String cypherVersionPrefix = "CYPHER " + cypherVersion; + Matcher matcher = CYPHER_VERSION_PATTERN.matcher(input); + if (matcher.find()) { + String cypher = matcher.group(1); // Always "CYPHER" + String version = matcher.group(2); // Optional version + return List.of( + version != null ? cypher + " " + version : cypher, + version != null ? cypher + " " + version : cypherVersionPrefix); + } + return List.of("", cypherVersionPrefix); // No prefix was found + } + + /** + * A helper function prefix a query with a Cypher Version, it will not check if the query is already prefixed or not. + * To do that call: prefixQueryWithCheck + * @param procedureCallContext the injectable context from the procedure framework + * @return The prefixed query + */ + public static String prefixQuery(ProcedureCallContext procedureCallContext, String query) { + return procedureCallContext.calledwithQueryLanguage().equals(QueryLanguage.CYPHER_5) + ? "CYPHER 5 " + query + : "CYPHER 25 " + query; + } + + public static String prefixQuery(String cypherVersion, String query) { + return "CYPHER " + cypherVersion + " " + query; + } + + public static String slottedRuntime(String cypherIterate, String cypherVersion) { + if (RUNTIME_PATTERN.matcher(cypherIterate).find()) { + return cypherIterate; + } + + return prependQueryOption(cypherIterate, CYPHER_RUNTIME_SLOTTED, cypherVersion); + } + + public enum Planner { + DEFAULT, + COST, + IDP, + DP + } + + public static String applyPlanner(String query, Planner planner, String cypherVersion) { + if (planner.equals(Planner.DEFAULT)) { + return Util.prefixQueryWithCheck(cypherVersion, query); + } + Matcher matcher = PLANNER_PATTERN.matcher(query); + String cypherPlanner = String.format(" planner=%s ", planner.name().toLowerCase()); + if (matcher.find()) { + return Util.prefixQueryWithCheck(cypherVersion, matcher.replaceFirst(cypherPlanner)); + } + return prependQueryOption(query, cypherPlanner, cypherVersion); + } + + public static String applyRuntime(String query, String runtime, String cypherVersion) { + Matcher matcher = RUNTIME_PATTERN.matcher(query); + String cypherRuntime = String.format(" runtime=%s ", runtime.toLowerCase()); + if (matcher.find()) { + return Util.prefixQueryWithCheck(cypherVersion, matcher.replaceFirst(cypherRuntime)); + } + return prependQueryOption(query, cypherRuntime, cypherVersion); + } + + private static String prependQueryOption(String query, String cypherOption, String cypherVersion) { + List cypherPrefix = Util.extractCypherPrefix(query, cypherVersion); + if (Objects.equals(cypherPrefix.getFirst(), "")) { + return cypherPrefix.get(1) + cypherOption + query; + } + return query.replaceFirst("(?i)" + cypherPrefix.getFirst(), cypherPrefix.get(1) + cypherOption); + } + // Get the current supported query language versions, if this list changes // this function will error, on error please update! public static List getCypherVersions() { diff --git a/core/src/main/java/apoc/atomic/Atomic.java b/core/src/main/java/apoc/atomic/Atomic.java index bc3e37ca2..84f9a5a31 100644 --- a/core/src/main/java/apoc/atomic/Atomic.java +++ b/core/src/main/java/apoc/atomic/Atomic.java @@ -34,6 +34,7 @@ import org.neo4j.graphdb.Entity; import org.neo4j.graphdb.NotFoundException; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.*; /** @@ -45,6 +46,9 @@ public class Atomic { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + /** * increment a property's value */ @@ -281,10 +285,10 @@ public Stream update( executionContext, (context) -> { oldValue[0] = entity.getProperty(property); - String statement = "WITH $container as n with n set n." + Util.sanitize(property, true) + "=" + String statement = "WITH $container AS n WITH n SET n." + Util.sanitize(property, true) + "=" + operation + ";"; Map properties = MapUtil.map("container", entity); - return context.tx.execute(statement, properties); + return context.tx.execute(Util.prefixQuery(procedureCallContext, statement), properties); }, retryAttempts); diff --git a/core/src/main/java/apoc/coll/Coll.java b/core/src/main/java/apoc/coll/Coll.java index b927b8dbd..45a402802 100644 --- a/core/src/main/java/apoc/coll/Coll.java +++ b/core/src/main/java/apoc/coll/Coll.java @@ -53,6 +53,7 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.impl.util.ValueUtils; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; @@ -69,6 +70,9 @@ public class Coll { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + @UserFunction("apoc.coll.stdev") @Description("Returns sample or population standard deviation with `isBiasCorrected` true or false respectively.") public Number stdev( @@ -177,8 +181,10 @@ public Object min(@Name(value = "values", description = "The list to find the mi if (list == null || list.isEmpty()) return null; if (list.size() == 1) return list.get(0); + var preparser = "CYPHER " + Util.getCypherVersionString(procedureCallContext) + " runtime=slotted "; try (Result result = tx.execute( - "cypher runtime=slotted return reduce(res=null, x in $list | CASE WHEN res IS NULL OR x list) { if (list == null || list.isEmpty()) return null; if (list.size() == 1) return list.get(0); + var preparser = "CYPHER " + Util.getCypherVersionString(procedureCallContext) + " runtime=slotted "; try (Result result = tx.execute( - "cypher runtime=slotted return reduce(res=null, x in $list | CASE WHEN res IS NULL OR res run( @Name(value = "statement", description = "The Cypher statement to run.") String statement, @Name(value = "params", description = "The parameters for the given Cypher statement.") Map params) { - return runCypherQuery(tx, statement, params); + return runCypherQuery(tx, statement, params, procedureCallContext); } @Procedure(name = "apoc.cypher.runMany", mode = WRITE) @@ -107,7 +111,8 @@ private Stream streamInNewTx(String cypher, Map doIt( @Name(value = "statement", description = "The Cypher statement to run.") String statement, @Name(value = "params", description = "The parameters for the given Cypher statement.") Map params) { - return runCypherQuery(tx, statement, params); + return runCypherQuery(tx, statement, params, procedureCallContext); } @Procedure(name = "apoc.cypher.runWrite", mode = WRITE) @@ -206,7 +211,7 @@ public Stream runSchema( @Name(value = "statement", description = "The Cypher schema statement to run.") String statement, @Name(value = "params", description = "The parameters for the given Cypher statement.") Map params) { - return runCypherQuery(tx, statement, params); + return runCypherQuery(tx, statement, params, procedureCallContext); } @NotThreadSafe @@ -231,8 +236,9 @@ public Stream when( if (targetQuery.isEmpty()) { return Stream.of(new CaseMapResult(Collections.emptyMap())); } else { - return tx.execute(withParamMapping(targetQuery, params.keySet()), params).stream() - .map(CaseMapResult::new); + String query = + Util.prefixQueryWithCheck(procedureCallContext, withParamMapping(targetQuery, params.keySet())); + return tx.execute(query, params).stream().map(CaseMapResult::new); } } @@ -294,16 +300,18 @@ public Stream whenCase( String ifQuery = (String) caseItr.next(); if (condition) { - return tx.execute(withParamMapping(ifQuery, params.keySet()), params).stream() - .map(CaseMapResult::new); + String query = + Util.prefixQueryWithCheck(procedureCallContext, withParamMapping(ifQuery, params.keySet())); + return tx.execute(query, params).stream().map(CaseMapResult::new); } } if (elseQuery.isEmpty()) { return Stream.of(new CaseMapResult(Collections.emptyMap())); } else { - return tx.execute(withParamMapping(elseQuery, params.keySet()), params).stream() - .map(CaseMapResult::new); + String query = + Util.prefixQueryWithCheck(procedureCallContext, withParamMapping(elseQuery, params.keySet())); + return tx.execute(query, params).stream().map(CaseMapResult::new); } } diff --git a/core/src/main/java/apoc/cypher/CypherFunctions.java b/core/src/main/java/apoc/cypher/CypherFunctions.java index aead1d243..75abaeb01 100644 --- a/core/src/main/java/apoc/cypher/CypherFunctions.java +++ b/core/src/main/java/apoc/cypher/CypherFunctions.java @@ -20,6 +20,7 @@ import static apoc.cypher.CypherUtils.withParamMapping; +import apoc.util.Util; import java.util.Collections; import java.util.List; import java.util.Map; @@ -27,6 +28,7 @@ import org.neo4j.graphdb.ResourceIterator; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; @@ -40,10 +42,13 @@ public class CypherFunctions { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + public Object runFirstColumn(String statement, Map params, boolean expectMultipleValues) { if (params == null) params = Collections.emptyMap(); String resolvedStatement = withParamMapping(statement, params.keySet()); - if (!resolvedStatement.contains(" runtime")) resolvedStatement = "cypher runtime=slotted " + resolvedStatement; + resolvedStatement = Util.slottedRuntime(resolvedStatement, Util.getCypherVersionString(procedureCallContext)); try (Result result = tx.execute(resolvedStatement, params)) { String firstColumn = result.columns().get(0); diff --git a/core/src/main/java/apoc/cypher/Timeboxed.java b/core/src/main/java/apoc/cypher/Timeboxed.java index 12ba20602..0b6a96caa 100644 --- a/core/src/main/java/apoc/cypher/Timeboxed.java +++ b/core/src/main/java/apoc/cypher/Timeboxed.java @@ -35,6 +35,7 @@ import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.TransactionTerminatedException; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.api.QueryLanguage; import org.neo4j.kernel.api.procedure.QueryLanguageScope; import org.neo4j.logging.Log; @@ -63,6 +64,9 @@ public class Timeboxed { @Context public TerminationGuard terminationGuard; + @Context + public ProcedureCallContext procedureCallContext; + private static final Map POISON = Collections.singletonMap("__magic", "POISON"); @NotThreadSafe @@ -105,7 +109,9 @@ public Stream runTimeboxed( pools.getDefaultExecutorService().submit(() -> { try (Transaction innerTx = db.beginTx()) { txAtomic.set(innerTx); - Result result = innerTx.execute(cypher, params == null ? Collections.EMPTY_MAP : params); + Result result = innerTx.execute( + Util.prefixQueryWithCheck(procedureCallContext, cypher), + params == null ? Collections.EMPTY_MAP : params); while (result.hasNext()) { if (Util.transactionIsTerminated(terminationGuard)) { txAtomic.get().close(); diff --git a/core/src/main/java/apoc/example/Examples.java b/core/src/main/java/apoc/example/Examples.java index 0d459c097..b07ad7127 100644 --- a/core/src/main/java/apoc/example/Examples.java +++ b/core/src/main/java/apoc/example/Examples.java @@ -23,6 +23,7 @@ import org.neo4j.graphdb.QueryStatistics; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Mode; @@ -34,6 +35,9 @@ public class Examples { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + public static class ExamplesProgressInfo { @Description("The name of the file containing the movies example.") public final String file; @@ -89,7 +93,7 @@ public ExamplesProgressInfo(long nodes, long relationships, long properties, lon public Stream movies() { long start = System.currentTimeMillis(); String file = "movies.cypher"; - Result result = tx.execute(Util.readResourceFile(file)); + Result result = tx.execute(Util.prefixQuery(procedureCallContext, Util.readResourceFile(file))); QueryStatistics stats = result.getQueryStatistics(); ExamplesProgressInfo progress = new ExamplesProgressInfo( stats.getNodesCreated(), diff --git a/core/src/main/java/apoc/export/csv/ExportCSV.java b/core/src/main/java/apoc/export/csv/ExportCSV.java index 8704240b9..ef174fd90 100644 --- a/core/src/main/java/apoc/export/csv/ExportCSV.java +++ b/core/src/main/java/apoc/export/csv/ExportCSV.java @@ -39,6 +39,7 @@ import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.impl.coreapi.InternalTransaction; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; @@ -67,6 +68,9 @@ public class ExportCSV { @Context public Pools pools; + @Context + public ProcedureCallContext procedureCallContext; + public ExportCSV() {} @NotThreadSafe @@ -191,6 +195,7 @@ public Stream query( : (Map) config.getOrDefault("params", Collections.emptyMap()); final String source; + query = Util.prefixQueryWithCheck(procedureCallContext, query); try (final var result = tx.execute(query, params)) { source = String.format("statement: cols(%d)", result.columns().size()); } diff --git a/core/src/main/java/apoc/export/cypher/ExportCypher.java b/core/src/main/java/apoc/export/cypher/ExportCypher.java index 51b2c1c34..79f46a764 100644 --- a/core/src/main/java/apoc/export/cypher/ExportCypher.java +++ b/core/src/main/java/apoc/export/cypher/ExportCypher.java @@ -43,6 +43,7 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; @@ -71,6 +72,9 @@ public class ExportCypher { @Context public Pools pools; + @Context + public ProcedureCallContext procedureCallContext; + @NotThreadSafe @Procedure("apoc.export.cypher.all") @Description( @@ -202,7 +206,7 @@ public Stream query( Map config) { if (Util.isNullOrEmpty(fileName)) fileName = null; ExportConfig c = new ExportConfig(config); - Result result = tx.execute(query); + Result result = tx.execute(Util.prefixQueryWithCheck(procedureCallContext, query)); SubGraph graph; graph = CypherResultSubGraph.from(tx, result, c.getRelsInBetween(), false); String source = String.format( diff --git a/core/src/main/java/apoc/export/graphml/ExportGraphML.java b/core/src/main/java/apoc/export/graphml/ExportGraphML.java index 13b8b0f2f..a3e217c7d 100644 --- a/core/src/main/java/apoc/export/graphml/ExportGraphML.java +++ b/core/src/main/java/apoc/export/graphml/ExportGraphML.java @@ -46,6 +46,7 @@ import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.security.URLAccessChecker; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Mode; @@ -77,6 +78,9 @@ public class ExportGraphML { @Context public URLAccessChecker urlAccessChecker; + @Context + public ProcedureCallContext procedureCallContext; + @Procedure(name = "apoc.import.graphml", mode = Mode.WRITE) @Description("Imports a graph from the provided GraphML file.") public Stream file( @@ -247,7 +251,7 @@ public Stream query( Map config) throws Exception { ExportConfig c = new ExportConfig(config); - Result result = tx.execute(query); + Result result = tx.execute(Util.prefixQueryWithCheck(procedureCallContext, query)); SubGraph graph = CypherResultSubGraph.from(tx, result, c.getRelsInBetween(), false); String source = String.format( "statement: nodes(%d), rels(%d)", diff --git a/core/src/main/java/apoc/export/json/ExportJson.java b/core/src/main/java/apoc/export/json/ExportJson.java index 6c2c07034..3a7cb0091 100644 --- a/core/src/main/java/apoc/export/json/ExportJson.java +++ b/core/src/main/java/apoc/export/json/ExportJson.java @@ -40,6 +40,7 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; @@ -63,6 +64,9 @@ public class ExportJson { @Context public TerminationGuard terminationGuard; + @Context + public ProcedureCallContext procedureCallContext; + @NotThreadSafe @Procedure("apoc.export.json.all") @Description("Exports the full database to the provided JSON file.") @@ -182,7 +186,7 @@ public Stream query( Map params = config == null ? Collections.emptyMap() : (Map) config.getOrDefault("params", Collections.emptyMap()); - Result result = tx.execute(query, params); + Result result = tx.execute(Util.prefixQueryWithCheck(procedureCallContext, query), params); String source = String.format("statement: cols(%d)", result.columns().size()); return exportJson(fileName, source, result, config); } diff --git a/core/src/main/java/apoc/graph/Graphs.java b/core/src/main/java/apoc/graph/Graphs.java index 45ee1492e..3fc2219b9 100644 --- a/core/src/main/java/apoc/graph/Graphs.java +++ b/core/src/main/java/apoc/graph/Graphs.java @@ -34,6 +34,7 @@ import org.neo4j.graphdb.Path; import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.*; /** @@ -45,6 +46,9 @@ public class Graphs { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + @Procedure("apoc.graph.fromData") @Description( "Generates a virtual sub-graph by extracting all of the `NODE` and `RELATIONSHIP` values from the given data.") @@ -130,7 +134,12 @@ public Stream fromCypher( Set nodes = new HashSet<>(1000); Set rels = new HashSet<>(1000); Map props = new HashMap<>(properties); - tx.execute(CypherUtils.withParamMapping(statement, params.keySet()), params).stream() + tx + .execute( + Util.prefixQueryWithCheck( + procedureCallContext, CypherUtils.withParamMapping(statement, params.keySet())), + params) + .stream() .forEach(row -> { row.forEach((k, v) -> { if (!extract(v, nodes, rels)) { diff --git a/core/src/main/java/apoc/help/Help.java b/core/src/main/java/apoc/help/Help.java index e6268b15b..6e6376ed6 100644 --- a/core/src/main/java/apoc/help/Help.java +++ b/core/src/main/java/apoc/help/Help.java @@ -20,11 +20,12 @@ import static apoc.util.Util.map; +import apoc.util.Util; import java.util.Map; import java.util.stream.Stream; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.api.QueryLanguage; -import org.neo4j.kernel.api.procedure.QueryLanguageScope; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; import org.neo4j.procedure.Name; @@ -36,27 +37,15 @@ public class Help { @Context public Transaction tx; - @NotThreadSafe - @Procedure("apoc.help") - @QueryLanguageScope(scope = {QueryLanguage.CYPHER_5}) - @Description( - "Returns descriptions of the available APOC procedures and functions. If a keyword is provided, it will return only those procedures and functions that have the keyword in their name.") - public Stream infoCypher5( - @Name(value = "proc", description = "A keyword to filter the results by.") String name) { - return help(name, true); - } + @Context + public ProcedureCallContext procedureCallContext; @NotThreadSafe @Procedure("apoc.help") - @QueryLanguageScope(scope = {QueryLanguage.CYPHER_25}) @Description( "Returns descriptions of the available APOC procedures and functions. If a keyword is provided, it will return only those procedures and functions that have the keyword in their name.") public Stream infoCypher25( @Name(value = "proc", description = "A keyword to filter the results by.") String name) { - return help(name, false); - } - - private Stream help(String name, Boolean version5) { boolean searchText = false; if (name != null) { name = name.trim(); @@ -65,16 +54,19 @@ private Stream help(String name, Boolean version5) { searchText = true; } } - String CypherPreparser = version5 ? "CYPHER 5 " : "CYPHER 25 "; String filter = " WHERE name starts with 'apoc.' " + " AND ($name IS NULL OR toLower(name) CONTAINS toLower($name) " + " OR ($desc IS NOT NULL AND toLower(description) CONTAINS toLower($desc))) "; - String proceduresQuery = CypherPreparser + "SHOW PROCEDURES yield name, description, signature, isDeprecated " - + filter + "RETURN 'procedure' as type, name, description, signature, isDeprecated "; + String proceduresQuery = Util.prefixQuery( + procedureCallContext, + "SHOW PROCEDURES yield name, description, signature, isDeprecated " + filter + + "RETURN 'procedure' as type, name, description, signature, isDeprecated "); - String functionsQuery = CypherPreparser + "SHOW FUNCTIONS yield name, description, signature, isDeprecated " - + filter + "RETURN 'function' as type, name, description, signature, isDeprecated "; + String functionsQuery = Util.prefixQuery( + procedureCallContext, + "SHOW FUNCTIONS yield name, description, signature, isDeprecated " + filter + + "RETURN 'function' as type, name, description, signature, isDeprecated "); Map params = map("name", name, "desc", searchText ? name : null); Stream> proceduresResults = tx.execute(proceduresQuery, params).stream(); Stream> functionsResults = tx.execute(functionsQuery, params).stream(); @@ -84,7 +76,7 @@ private Stream help(String name, Boolean version5) { row, existsInCore( (String) row.get("name"), - version5, + procedureCallContext.calledwithQueryLanguage().equals(QueryLanguage.CYPHER_5), row.get("type").equals("function"))))); } diff --git a/core/src/main/java/apoc/merge/Merge.java b/core/src/main/java/apoc/merge/Merge.java index d8bb1f469..d5aabb025 100644 --- a/core/src/main/java/apoc/merge/Merge.java +++ b/core/src/main/java/apoc/merge/Merge.java @@ -34,6 +34,7 @@ import org.neo4j.graphdb.Relationship; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.procedure.*; public class Merge { @@ -41,6 +42,9 @@ public class Merge { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + @Procedure(value = "apoc.merge.node.eager", mode = Mode.WRITE, eager = true) @Description("Merges the given `NODE` values with the given dynamic labels eagerly.") public Stream nodesEager( @@ -151,8 +155,10 @@ private Result getNodeResult( Util.map("identProps", identProps, "onCreateProps", onCreateProps, "onMatchProps", onMatchProps); String identPropsString = buildIdentPropsString(identProps); - final String cypher = "MERGE (n" + labels + "{" + identPropsString - + "}) ON CREATE SET n += $onCreateProps ON MATCH SET n += $onMatchProps RETURN n"; + final String cypher = Util.prefixQuery( + procedureCallContext, + "MERGE (n" + labels + "{" + identPropsString + + "}) ON CREATE SET n += $onCreateProps ON MATCH SET n += $onMatchProps RETURN n"); return tx.execute(cypher, params); } @@ -227,7 +233,7 @@ private Result getRelResult( + Util.quote(relType) + "{" + identPropsString + "}]->(endNode) " + "ON CREATE SET r+= $onCreateProps " + "ON MATCH SET r+= $onMatchProps " + "RETURN r"; - return tx.execute(cypher, params); + return tx.execute(Util.prefixQuery(procedureCallContext, cypher), params); } @Procedure(value = "apoc.merge.relationship.eager", mode = Mode.WRITE, eager = true) diff --git a/core/src/main/java/apoc/meta/Meta.java b/core/src/main/java/apoc/meta/Meta.java index 1709e3562..ed20d3ebc 100644 --- a/core/src/main/java/apoc/meta/Meta.java +++ b/core/src/main/java/apoc/meta/Meta.java @@ -30,6 +30,7 @@ import apoc.result.VirtualRelationship; import apoc.util.CollectionUtils; import apoc.util.MapUtil; +import apoc.util.Util; import apoc.util.collection.Iterables; import com.google.common.collect.Sets; import java.util.AbstractMap; @@ -70,6 +71,7 @@ import org.neo4j.graphdb.schema.Schema; import org.neo4j.internal.kernel.api.Read; import org.neo4j.internal.kernel.api.TokenRead; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.logging.Log; import org.neo4j.procedure.Context; @@ -98,6 +100,9 @@ private record MetadataKey(Types type, String key) {} @Context public Log log; + @Context + public ProcedureCallContext procedureCallContext; + /** * Represents the result of a metadata operation. */ @@ -503,7 +508,7 @@ public Stream dataOf( MetaConfig metaConfig = new MetaConfig(config); final SubGraph subGraph; if (graph instanceof String) { - Result result = tx.execute((String) graph); + Result result = tx.execute(Util.prefixQueryWithCheck(procedureCallContext, (String) graph)); subGraph = CypherResultSubGraph.from(tx, result, metaConfig.isAddRelationshipsBetweenNodes()); } else if (graph instanceof Map) { Map mGraph = (Map) graph; @@ -1132,8 +1137,9 @@ public Stream graphOf( Map config) { MetaConfig metaConfig = new MetaConfig(config, false); final SubGraph subGraph; - if (graph instanceof String) { - Result result = tx.execute("CYPHER runtime=pipelined " + (String) graph); + if (graph instanceof String query) { + Result result = tx.execute( + Util.applyRuntime(query, "pipelined", Util.getCypherVersionString(procedureCallContext))); subGraph = CypherResultSubGraph.from(tx, result, metaConfig.isAddRelationshipsBetweenNodes()); } else if (graph instanceof Map) { Map mGraph = (Map) graph; diff --git a/core/src/main/java/apoc/nodes/Nodes.java b/core/src/main/java/apoc/nodes/Nodes.java index 8e81a592d..d1254231a 100644 --- a/core/src/main/java/apoc/nodes/Nodes.java +++ b/core/src/main/java/apoc/nodes/Nodes.java @@ -67,6 +67,7 @@ import org.neo4j.internal.kernel.api.Read; import org.neo4j.internal.kernel.api.RelationshipTraversalCursor; import org.neo4j.internal.kernel.api.TokenRead; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.kernel.impl.coreapi.InternalTransaction; import org.neo4j.procedure.Context; @@ -92,6 +93,9 @@ public class Nodes { @Context public Pools pools; + @Context + public ProcedureCallContext procedureCallContext; + public static class CyclesPathResult { @Description("A path containing a found cycle.") public Path path; @@ -219,7 +223,9 @@ public Stream delete( final List batch = Util.take(it, (int) batchSize); count += Util.inTx(db, pools, (txInThread) -> { txInThread - .execute("FOREACH (n in $nodes | DETACH DELETE n)", map("nodes", batch)) + .execute( + Util.prefixQuery(procedureCallContext, "FOREACH (n in $nodes | DETACH DELETE n)"), + map("nodes", batch)) .close(); return batch.size(); }); @@ -766,9 +772,11 @@ public boolean isDeleted( if (object == null) return true; final String query; if (object instanceof Node) { - query = "MATCH (n) WHERE elementId(n) = $id RETURN COUNT(n) = 1 AS exists"; + query = Util.prefixQuery( + procedureCallContext, "MATCH (n) WHERE elementId(n) = $id RETURN COUNT(n) = 1 AS exists"); } else if (object instanceof Relationship) { - query = "MATCH ()-[r]->() WHERE elementId(r) = $id RETURN COUNT(r) = 1 AS exists"; + query = Util.prefixQuery( + procedureCallContext, "MATCH ()-[r]->() WHERE elementId(r) = $id RETURN COUNT(r) = 1 AS exists"); } else { throw new IllegalArgumentException( "expected Node or Relationship but was " + object.getClass().getSimpleName()); diff --git a/core/src/main/java/apoc/periodic/Periodic.java b/core/src/main/java/apoc/periodic/Periodic.java index 32bf08a18..2831c48c2 100644 --- a/core/src/main/java/apoc/periodic/Periodic.java +++ b/core/src/main/java/apoc/periodic/Periodic.java @@ -33,7 +33,6 @@ import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; -import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; import org.apache.commons.lang3.tuple.Pair; @@ -43,24 +42,13 @@ import org.neo4j.graphdb.schema.ConstraintDefinition; import org.neo4j.graphdb.schema.IndexDefinition; import org.neo4j.graphdb.schema.Schema; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.impl.coreapi.InternalTransaction; import org.neo4j.logging.Log; import org.neo4j.procedure.*; public class Periodic { - enum Planner { - DEFAULT, - COST, - IDP, - DP - } - - public static final Pattern PLANNER_PATTERN = - Pattern.compile("\\bplanner\\s*=\\s*[^\\s]*", Pattern.CASE_INSENSITIVE); - public static final Pattern RUNTIME_PATTERN = Pattern.compile("\\bruntime\\s*=", Pattern.CASE_INSENSITIVE); - public static final Pattern CYPHER_PREFIX_PATTERN = Pattern.compile("^\\s*\\bcypher\\b", Pattern.CASE_INSENSITIVE); - public static final String CYPHER_RUNTIME_SLOTTED = " runtime=slotted "; static final Pattern LIMIT_PATTERN = Pattern.compile("\\slimit\\s", Pattern.CASE_INSENSITIVE); @Context @@ -78,6 +66,9 @@ enum Planner { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + @Admin @Procedure(name = "apoc.periodic.truncate", mode = Mode.SCHEMA) @Description( @@ -233,10 +224,13 @@ public RundownResult( private long executeNumericResultStatement( @Name("statement") String statement, @Name("params") Map parameters) { - return db.executeTransactionally(statement, parameters, result -> { - String column = Iterables.single(result.columns()); - return result.columnAs(column).stream().mapToLong(o -> (long) o).sum(); - }); + return db.executeTransactionally( + Util.prefixQueryWithCheck(procedureCallContext, statement), parameters, result -> { + String column = Iterables.single(result.columns()); + return result.columnAs(column).stream() + .mapToLong(o -> (long) o) + .sum(); + }); } @Procedure("apoc.periodic.cancel") @@ -259,7 +253,8 @@ public Stream submit( @Name(value = "params", defaultValue = "{}", description = "{ params = {} :: MAP }") Map config) { validateQuery(statement); - return submitProc(name, statement, config, db, log, pools); + var query = Util.prefixQueryWithCheck(procedureCallContext, statement); + return submitProc(name, query, config, db, log, pools); } @Procedure(name = "apoc.periodic.repeat", mode = Mode.WRITE) @@ -276,7 +271,8 @@ public Stream repeat( name, () -> { // `resultAsString` in order to consume result - db.executeTransactionally(statement, params, Result::resultAsString); + db.executeTransactionally( + Util.prefixQueryWithCheck(procedureCallContext, statement), params, Result::resultAsString); }, 0, rate); @@ -305,7 +301,8 @@ public Stream countdown( @Name(value = "delay", description = "The delay in seconds to wait between each job execution.") long delay) { validateQuery(statement); - JobInfo info = submitJob(name, new Countdown(name, statement, delay, log), log, pools); + var query = Util.prefixQueryWithCheck(procedureCallContext, statement); + JobInfo info = submitJob(name, new Countdown(name, query, delay, log), log, pools); info.delay = delay; return Stream.of(info); } @@ -382,11 +379,14 @@ public Stream iterate( BatchMode batchMode = BatchMode.fromConfig(config); Map params = (Map) config.getOrDefault("params", Collections.emptyMap()); - try (Result result = tx.execute(slottedRuntime(cypherIterate), params)) { + try (Result result = tx.execute( + Util.slottedRuntime(cypherIterate, Util.getCypherVersionString(procedureCallContext)), params)) { Pair prepared = PeriodicUtils.prepareInnerStatement(cypherAction, batchMode, result.columns(), "_batch"); - String innerStatement = applyPlanner(prepared.getLeft(), Planner.valueOf((String) - config.getOrDefault("planner", Planner.DEFAULT.name()))); + String innerStatement = Util.applyPlanner( + prepared.getLeft(), + Util.Planner.valueOf((String) config.getOrDefault("planner", Util.Planner.DEFAULT.name())), + Util.getCypherVersionString(procedureCallContext)); boolean iterateList = prepared.getRight(); String periodicId = UUID.randomUUID().toString(); if (log.isDebugEnabled()) { @@ -418,34 +418,6 @@ public Stream iterate( } } - static String slottedRuntime(String cypherIterate) { - if (RUNTIME_PATTERN.matcher(cypherIterate).find()) { - return cypherIterate; - } - - return prependQueryOption(cypherIterate, CYPHER_RUNTIME_SLOTTED); - } - - public static String applyPlanner(String query, Planner planner) { - if (planner.equals(Planner.DEFAULT)) { - return query; - } - Matcher matcher = PLANNER_PATTERN.matcher(query); - String cypherPlanner = String.format(" planner=%s ", planner.name().toLowerCase()); - if (matcher.find()) { - return matcher.replaceFirst(cypherPlanner); - } - return prependQueryOption(query, cypherPlanner); - } - - private static String prependQueryOption(String query, String cypherOption) { - String cypherPrefix = "cypher"; - String completePrefix = cypherPrefix + cypherOption; - return CYPHER_PREFIX_PATTERN.matcher(query).find() - ? query.replaceFirst("(?i)" + cypherPrefix, completePrefix) - : completePrefix + query; - } - private class Countdown implements Runnable { private final String name; private final String statement; diff --git a/core/src/main/java/apoc/refactor/GraphRefactoring.java b/core/src/main/java/apoc/refactor/GraphRefactoring.java index 89e21c262..472280788 100644 --- a/core/src/main/java/apoc/refactor/GraphRefactoring.java +++ b/core/src/main/java/apoc/refactor/GraphRefactoring.java @@ -38,6 +38,7 @@ import java.util.stream.Stream; import org.neo4j.graphdb.*; import org.neo4j.graphdb.schema.ConstraintType; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.api.QueryLanguage; import org.neo4j.kernel.api.procedure.QueryLanguageScope; import org.neo4j.kernel.impl.coreapi.InternalTransaction; @@ -57,6 +58,9 @@ public class GraphRefactoring { @Context public Pools pools; + @Context + public ProcedureCallContext procedureCallContext; + @Procedure(name = "apoc.refactor.extractNode", mode = Mode.WRITE) @Description("Expands the given `RELATIONSHIP` VALUES into intermediate `NODE` VALUES.\n" + "The intermediate `NODE` values are connected by the given `outType` and `inType`.") @@ -716,6 +720,7 @@ public record RefactorGraphResult( mode = Mode.WRITE, deprecatedBy = "Deprecated for removal without a direct replacement, use plain Cypher or create a custom procedure.") + @Deprecated @Description( """ Removes the given `NODE` values from the `PATH` (and graph, including all of its relationships) and reconnects the remaining `NODE` values. @@ -828,7 +833,7 @@ public Stream deleteAndReconnectCypher5( rels.removeAll(List.of(relationshipIn, relationshipOut)); } - tx.execute("WITH $node as n DETACH DELETE n", Map.of("node", node)); + tx.execute(Util.prefixQuery(procedureCallContext, "WITH $node as n DETACH DELETE n"), Map.of("node", node)); nodes.remove(node); }); @@ -870,7 +875,7 @@ private Future categorizeNodes( Map params = new HashMap<>(2); params.put("node", node); params.put("value", value); - Result result = innerTx.execute(q, params); + Result result = innerTx.execute(Util.prefixQuery(procedureCallContext, q), params); if (result.hasNext()) { Node cat = (Node) result.next().get("cat"); for (String copiedKey : copiedKeys) { diff --git a/core/src/main/java/apoc/refactor/rename/Rename.java b/core/src/main/java/apoc/refactor/rename/Rename.java index 6b2fc0650..5f8039286 100644 --- a/core/src/main/java/apoc/refactor/rename/Rename.java +++ b/core/src/main/java/apoc/refactor/rename/Rename.java @@ -38,6 +38,7 @@ import org.neo4j.graphdb.Transaction; import org.neo4j.graphdb.schema.ConstraintDefinition; import org.neo4j.graphdb.schema.IndexDefinition; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.logging.Log; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; @@ -71,6 +72,9 @@ public class Rename { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + /** * Rename the Label of a node by creating a new one and deleting the old. */ @@ -270,6 +274,7 @@ private Periodic newPeriodic() { periodic.terminationGuard = this.terminationGuard; periodic.pools = this.pools; periodic.tx = this.tx; + periodic.procedureCallContext = this.procedureCallContext; return periodic; } diff --git a/core/src/main/java/apoc/schema/Schemas.java b/core/src/main/java/apoc/schema/Schemas.java index 2b3529dbd..5e3a81383 100644 --- a/core/src/main/java/apoc/schema/Schemas.java +++ b/core/src/main/java/apoc/schema/Schemas.java @@ -61,6 +61,7 @@ import org.neo4j.internal.kernel.api.TokenRead; import org.neo4j.internal.kernel.api.exceptions.LabelNotFoundKernelException; import org.neo4j.internal.kernel.api.exceptions.schema.IndexNotFoundKernelException; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.internal.schema.IndexDescriptor; import org.neo4j.kernel.api.KernelTransaction; import org.neo4j.kernel.api.QueryLanguage; @@ -84,6 +85,9 @@ public class Schemas { @Context public KernelTransaction ktx; + @Context + public ProcedureCallContext procedureCallContext; + @NotThreadSafe @Procedure(name = "apoc.schema.assert", mode = Mode.SCHEMA) @Description("Drops all other existing indexes and constraints when `dropExisting` is `true` (default is `true`).\n" @@ -104,7 +108,8 @@ public Stream schemaAssert( description = "Whether or not to drop all other existing indexes and constraints.") boolean dropExisting) { return Stream.concat( - assertIndexes(indexes, dropExisting).stream(), assertConstraints(constraints, dropExisting).stream()); + assertIndexes(indexes, dropExisting, Util.getCypherVersionString(procedureCallContext)).stream(), + assertConstraints(constraints, dropExisting).stream()); } @NotThreadSafe @@ -306,7 +311,8 @@ private AssertSchemaResult createUniqueConstraint(Schema schema, String lbl, Str return new AssertSchemaResult(lbl, key).unique().created(); } - public List assertIndexes(Map> indexes0, boolean dropExisting) + public List assertIndexes( + Map> indexes0, boolean dropExisting, String cypherVersion) throws IllegalArgumentException { Schema schema = tx.schema(); Map> indexes = copyMapOfObjects(indexes0); @@ -350,7 +356,7 @@ public List assertIndexes(Map> indexes0 if (key instanceof String) { result.add(createSinglePropertyIndex(schema, index.getKey(), (String) key)); } else if (key instanceof List) { - result.add(createCompoundIndex(index.getKey(), (List) key)); + result.add(createCompoundIndex(index.getKey(), (List) key, cypherVersion)); } } } @@ -376,12 +382,13 @@ private AssertSchemaResult createSinglePropertyIndex(Schema schema, String lbl, return new AssertSchemaResult(lbl, key).created(); } - private AssertSchemaResult createCompoundIndex(String label, List keys) { + private AssertSchemaResult createCompoundIndex(String label, List keys, String cypherVersion) { List backTickedKeys = new ArrayList<>(); keys.forEach(key -> backTickedKeys.add(String.format("n.`%s`", Util.sanitize(key)))); tx.execute(String.format( - "CREATE INDEX FOR (n:`%s`) ON (%s)", Util.sanitize(label), String.join(",", backTickedKeys))) + "CYPHER %s CREATE INDEX FOR (n:`%s`) ON (%s)", + cypherVersion, Util.sanitize(label), String.join(",", backTickedKeys))) .close(); return new AssertSchemaResult(label, keys).created(); } diff --git a/core/src/main/java/apoc/search/ParallelNodeSearch.java b/core/src/main/java/apoc/search/ParallelNodeSearch.java index 16a4ae507..1858cc57f 100644 --- a/core/src/main/java/apoc/search/ParallelNodeSearch.java +++ b/core/src/main/java/apoc/search/ParallelNodeSearch.java @@ -34,6 +34,7 @@ import org.neo4j.graphdb.Node; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.logging.Log; import org.neo4j.procedure.Context; import org.neo4j.procedure.Description; @@ -55,6 +56,9 @@ public class ParallelNodeSearch { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + @NotThreadSafe @Procedure("apoc.search.nodeAllReduced") @Description( @@ -209,10 +213,25 @@ private Stream createWorkersFromValidInput( String label = e.getKey(); Object properties = e.getValue(); if (properties instanceof String) { - return Stream.of(new QueryWorker(api, label, (String) properties, operator, value, log)); + return Stream.of(new QueryWorker( + api, + label, + (String) properties, + operator, + value, + log, + Util.getCypherVersionString(procedureCallContext))); } else if (properties instanceof List) { return ((List) properties) - .stream().map(prop -> new QueryWorker(api, label, prop, operator, value, log)); + .stream() + .map(prop -> new QueryWorker( + api, + label, + prop, + operator, + value, + log, + Util.getCypherVersionString(procedureCallContext))); } throw new RuntimeException("Invalid type for properties " + properties + ": " + (properties == null ? "null" : properties.getClass())); @@ -224,21 +243,32 @@ public static class QueryWorker { private String label, prop, operator; Object value; private Log log; + private String cypherVersion; - public QueryWorker(GraphDatabaseService db, String label, String prop, String operator, Object value, Log log) { + public QueryWorker( + GraphDatabaseService db, + String label, + String prop, + String operator, + Object value, + Log log, + String cypherVersion) { this.db = db; this.label = label; this.prop = prop; this.value = value; this.operator = operator; this.log = log; + this.cypherVersion = cypherVersion; } public Stream queryForData() { List labels = singletonList(label); - String query = format( - "match (n:`%s`) where n.`%s` %s $value return id(n) as id, n.`%s` as value", - label, prop, operator, prop); + String query = Util.prefixQuery( + cypherVersion, + format( + "MATCH (n:`%s`) WHERE n.`%s` %s $value RETURN id(n) AS id, n.`%s` AS value", + label, prop, operator, prop)); return queryForNode( query, (row) -> new NodeReducedResult((long) row.get("id"), labels, singletonMap(prop, row.get("value")))) @@ -246,7 +276,9 @@ public Stream queryForData() { } public Stream queryForNodeId() { - String query = format("match (n:`%s`) where n.`%s` %s $value return id(n) AS id", label, prop, operator); + String query = Util.prefixQuery( + cypherVersion, + format("MATCH (n:`%s`) WHERE n.`%s` %s $value RETURN id(n) AS id", label, prop, operator)); return queryForNode(query, (row) -> (long) row.get("id")).stream(); } diff --git a/core/src/main/java/apoc/trigger/Trigger.java b/core/src/main/java/apoc/trigger/Trigger.java index 3899c52d0..4e31de8b8 100644 --- a/core/src/main/java/apoc/trigger/Trigger.java +++ b/core/src/main/java/apoc/trigger/Trigger.java @@ -90,7 +90,8 @@ public Stream add( Util.validateQuery(db, statement); Map params = (Map) config.getOrDefault("params", Collections.emptyMap()); - Map removed = triggerHandler.add(name, statement, selector, params); + var query = Util.prefixQueryWithCheck("5", statement); + Map removed = triggerHandler.add(name, query, selector, params); if (removed != null) { return Stream.of( new TriggerInfo( diff --git a/core/src/main/java/apoc/trigger/TriggerNewProcedures.java b/core/src/main/java/apoc/trigger/TriggerNewProcedures.java index 2eea687b1..1fa51d910 100644 --- a/core/src/main/java/apoc/trigger/TriggerNewProcedures.java +++ b/core/src/main/java/apoc/trigger/TriggerNewProcedures.java @@ -29,6 +29,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.neo4j.graphdb.Transaction; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; import org.neo4j.kernel.api.procedure.SystemProcedure; import org.neo4j.kernel.internal.GraphDatabaseAPI; import org.neo4j.procedure.Admin; @@ -51,6 +52,9 @@ public class TriggerNewProcedures { @Context public Transaction tx; + @Context + public ProcedureCallContext procedureCallContext; + private void checkInSystemWriter() { TriggerHandlerNewProcedures.checkEnabled(); @@ -102,10 +106,10 @@ public Stream install( checkTargetDatabase(databaseName); Map params = (Map) config.getOrDefault("params", Collections.emptyMap()); + var query = Util.prefixQueryWithCheck(procedureCallContext, statement); return withUpdatingTransaction( databaseName, - tx -> Stream.of( - TriggerHandlerNewProcedures.install(databaseName, name, statement, selector, params, tx))); + tx -> Stream.of(TriggerHandlerNewProcedures.install(databaseName, name, query, selector, params, tx))); } // TODO - change with @SystemOnlyProcedure diff --git a/core/src/test/java/apoc/HelperProcedures.java b/core/src/test/java/apoc/HelperProcedures.java new file mode 100644 index 000000000..197bad117 --- /dev/null +++ b/core/src/test/java/apoc/HelperProcedures.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) "Neo4j" + * Neo4j Sweden AB [http://neo4j.com] + * + * This file is part of Neo4j. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package apoc; + +import java.util.List; +import org.neo4j.internal.kernel.api.procs.ProcedureCallContext; +import org.neo4j.procedure.Context; +import org.neo4j.procedure.Description; +import org.neo4j.procedure.UserFunction; + +public class HelperProcedures { + + @Context + public ProcedureCallContext procedureCallContext; + + public static class CypherVersionCombinations { + public String outerVersion; + public String innerVersion; + public String result; + + public CypherVersionCombinations(String outerVersion, String innerVersion, String result) { + this.outerVersion = outerVersion; + this.innerVersion = innerVersion; + this.result = result; + } + } + + public static final List cypherVersions = List.of( + new CypherVersionCombinations("CYPHER 5", "", "CYPHER_5"), + new CypherVersionCombinations("CYPHER 5", "CYPHER 5", "CYPHER_5"), + new CypherVersionCombinations("CYPHER 5", "CYPHER 25", "CYPHER_25"), + new CypherVersionCombinations("CYPHER 25", "", "CYPHER_25"), + new CypherVersionCombinations("CYPHER 25", "CYPHER 25", "CYPHER_25"), + new CypherVersionCombinations("CYPHER 25", "CYPHER 5", "CYPHER_5")); + + @UserFunction(name = "apoc.cypherVersion") + @Description("This test function returns a string of the Cypher Version that is was called with.") + public String cypherVersion() { + return procedureCallContext.calledwithQueryLanguage().name(); + } +} diff --git a/core/src/test/java/apoc/cypher/CypherTest.java b/core/src/test/java/apoc/cypher/CypherTest.java index 094dcaaa1..2ca28a5e2 100644 --- a/core/src/test/java/apoc/cypher/CypherTest.java +++ b/core/src/test/java/apoc/cypher/CypherTest.java @@ -40,6 +40,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import apoc.HelperProcedures; import apoc.text.Strings; import apoc.util.TestUtil; import apoc.util.Util; @@ -88,7 +89,13 @@ public class CypherTest { public static void setUp() { apocConfig().setProperty(APOC_IMPORT_FILE_ENABLED, true); TestUtil.registerProcedure( - db, Cypher.class, Utils.class, CypherFunctions.class, Timeboxed.class, Strings.class); + db, + Cypher.class, + Utils.class, + CypherFunctions.class, + Timeboxed.class, + Strings.class, + HelperProcedures.class); } @After @@ -621,4 +628,219 @@ private void assertNoOpenTransactions() { "show transactions", Map.of(), r -> r.stream().toList()); assertThat(txs).satisfiesExactly(row -> assertEquals("show transactions", row.get("currentQuery"))); } + + @Test + public void testDifferentCypherVersionsApocCase() { + // Test if case + for (String procName : List.of("case", "do.case")) { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.%s([true, '%s RETURN apoc.cypherVersion() AS version']) + """, + cypherVersion.outerVersion, procName, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + + // Test else case + for (String procName : List.of("case", "do.case")) { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.%s([false, 'RETURN 1 AS version'], '%s RETURN apoc.cypherVersion() AS version') + """, + cypherVersion.outerVersion, procName, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + } + + @Test + public void testDifferentCypherVersionsApocWhen() { + // Test if case + for (String procName : List.of("when", "do.when")) { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.%s(true, '%s RETURN apoc.cypherVersion() AS version', 'RETURN 1') + """, + cypherVersion.outerVersion, procName, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + + // Test else case + for (String procName : List.of("when", "do.when")) { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.%s(false, 'RETURN 1 AS version', '%s RETURN apoc.cypherVersion() AS version') + """, + cypherVersion.outerVersion, procName, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + } + + @Test + public void testDifferentCypherVersionsApocDoIt() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.cypher.doIt('%s RETURN apoc.cypherVersion() AS version', {}) + """, + cypherVersion.outerVersion, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + + @Test + public void testDifferentCypherVersionsApocRun() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.cypher.run('%s RETURN apoc.cypherVersion() AS version', {}) + """, + cypherVersion.outerVersion, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + + @Test + public void testDifferentCypherVersionsApocRunMany() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testResult( + db, + String.format( + """ + %s + CALL apoc.cypher.runMany('%s RETURN apoc.cypherVersion() AS version;\n %s RETURN apoc.cypherVersion() AS version;', {}, {statistics: false}) + """, + cypherVersion.outerVersion, cypherVersion.innerVersion, cypherVersion.innerVersion), + r -> { + assertTrue(r.hasNext()); + Map row = r.next(); + assertEquals(cypherVersion.result, ((Map) row.get("result")).get("version")); + assertTrue(r.hasNext()); + row = r.next(); + assertEquals(cypherVersion.result, ((Map) row.get("result")).get("version")); + assertFalse(r.hasNext()); + }); + } + } + + @Test + public void testDifferentCypherVersionsApocRunManyReadOnly() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testResult( + db, + String.format( + """ + %s + CALL apoc.cypher.runManyReadOnly('%s RETURN apoc.cypherVersion() AS version;\n %s RETURN apoc.cypherVersion() AS version;', {}, {statistics: false}) + """, + cypherVersion.outerVersion, cypherVersion.innerVersion, cypherVersion.innerVersion), + r -> { + assertTrue(r.hasNext()); + Map row = r.next(); + assertEquals(cypherVersion.result, ((Map) row.get("result")).get("version")); + assertTrue(r.hasNext()); + row = r.next(); + assertEquals(cypherVersion.result, ((Map) row.get("result")).get("version")); + assertFalse(r.hasNext()); + }); + } + } + + @Test + public void testDifferentCypherVersionsApocRunTimeboxed() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.cypher.runTimeboxed('%s RETURN apoc.cypherVersion() AS version;', {}, 100000) + """, + cypherVersion.outerVersion, cypherVersion.innerVersion, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + + @Test + public void testDifferentCypherVersionsApocRunWrite() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + CALL apoc.cypher.runWrite('%s CREATE (n:Test {prop: apoc.cypherVersion()}) RETURN n.prop AS version;', {}) + """, + cypherVersion.outerVersion, cypherVersion.innerVersion, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, ((Map) r.get("value")).get("version"))); + } + } + + @Test + public void testDifferentCypherVersionsApocRunFirstColumnSingle() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + RETURN apoc.cypher.runFirstColumnSingle('%s RETURN apoc.cypherVersion() AS version', {}) AS value + """, + cypherVersion.outerVersion, cypherVersion.innerVersion, cypherVersion.innerVersion), + r -> assertEquals(cypherVersion.result, r.get("value"))); + } + } + + @Test + public void testDifferentCypherVersionsApocRunFirstColumnMany() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCall( + db, + String.format( + """ + %s + RETURN apoc.cypher.runFirstColumnMany('%s UNWIND [1, 2] AS a RETURN apoc.cypherVersion() AS version', {}) AS value + """, + cypherVersion.outerVersion, cypherVersion.innerVersion), + r -> assertEquals(Arrays.asList(cypherVersion.result, cypherVersion.result), r.get("value"))); + } + } + + @Test + public void testDifferentCypherVersionsApocRunSchema() { + // This doesn't return anything, so just check it doesn't error :) + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + testCallEmpty( + db, + String.format( + "%s CALL apoc.cypher.runSchema('%s CREATE INDEX test IF NOT EXISTS FOR (w:Test) ON (w.name)',{})", + cypherVersion.outerVersion, cypherVersion.innerVersion), + Collections.emptyMap()); + } + } } diff --git a/core/src/test/java/apoc/export/csv/ExportCsvTest.java b/core/src/test/java/apoc/export/csv/ExportCsvTest.java index 926f42267..2c46299fa 100644 --- a/core/src/test/java/apoc/export/csv/ExportCsvTest.java +++ b/core/src/test/java/apoc/export/csv/ExportCsvTest.java @@ -27,6 +27,7 @@ import static apoc.util.CompressionAlgo.NONE; import static apoc.util.MapUtil.map; import static apoc.util.TestUtil.assertError; +import static apoc.util.TestUtil.testCall; import static apoc.util.TestUtil.testResult; import static apoc.util.Util.INVALID_QUERY_MODE_ERROR; import static java.nio.charset.StandardCharsets.UTF_8; @@ -36,6 +37,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import apoc.HelperProcedures; import apoc.csv.CsvTestUtil; import apoc.graph.Graphs; import apoc.meta.Meta; @@ -60,6 +62,7 @@ import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.graphdb.QueryExecutionException; import org.neo4j.graphdb.Result; @@ -440,11 +443,13 @@ public class ExportCsvTest { public static DbmsRule db = new ImpermanentDbmsRule() .withSetting( GraphDatabaseSettings.load_csv_file_url_root, - directory.toPath().toAbsolutePath()); + directory.toPath().toAbsolutePath()) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true); @BeforeClass public static void setUp() { - TestUtil.registerProcedure(db, ExportCSV.class, Graphs.class, Meta.class, ImportCsv.class); + TestUtil.registerProcedure( + db, ExportCSV.class, Graphs.class, Meta.class, ImportCsv.class, HelperProcedures.class); apocConfig().setProperty(APOC_IMPORT_FILE_ENABLED, true); apocConfig().setProperty(APOC_EXPORT_FILE_ENABLED, true); db.executeTransactionally( @@ -477,7 +482,7 @@ private String readFile(String fileName, Charset charset, CompressionAlgo compre public void testExportInvalidQuoteValue() { try { String fileName = "all.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{quotes: 'Invalid'})", map("file", fileName), @@ -494,7 +499,7 @@ public void testExportInvalidQuoteValue() { public void textExportWithTypes() { db.executeTransactionally( "CREATE (n:TestNode) SET n = {valFloat:toFloat(123), name:'NodeName', valInt:5, dateVal: date('2024-11-01')};"); - TestUtil.testCall( + testCall( db, """ CALL apoc.graph.fromCypher("MATCH (n:TestNode) RETURN n", {}, 'TestNode.csv',{}) YIELD graph @@ -524,7 +529,7 @@ public void textExportWithTypes() { public void testExportAllCsvCompressed() { final CompressionAlgo compressionAlgo = DEFLATE; String fileName = "all.csv.zz"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file, $config)", map("file", fileName, "config", map("compression", compressionAlgo.name())), @@ -536,7 +541,7 @@ public void testExportAllCsvCompressed() { public void testConsistentQuotingAlways() { // All in one file String fileName1 = "allOneFileAlways.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: false})", map("file", fileName1), @@ -546,7 +551,7 @@ public void testConsistentQuotingAlways() { // In separate files String fileNameStart = "allBulkImportAlways"; String fileName2 = fileNameStart + ".csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: true})", map("file", fileName2), @@ -561,7 +566,7 @@ public void testConsistentQuotingAlways() { assertEquals(EXPECTED_ALL_ALWAYS_REL, readFile(fileNameStart + ".relationships.REL.csv")); // Streaming - TestUtil.testCall(db, "CALL apoc.export.csv.all(null,{stream: true})", (r) -> { + testCall(db, "CALL apoc.export.csv.all(null,{stream: true})", (r) -> { String data = (String) r.get("data"); assertEquals(EXPECTED_ALL_ALWAYS, data); }); @@ -571,7 +576,7 @@ public void testConsistentQuotingAlways() { public void testConsistentQuotingIfNeeded() { // All in one file String fileName1 = "allOneFileIfNeeded.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: false, quotes: 'ifNeeded'})", map("file", fileName1), @@ -581,7 +586,7 @@ public void testConsistentQuotingIfNeeded() { // In separate files String fileNameStart = "allBulkImportIfNeeded"; String fileName2 = fileNameStart + ".csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: true, quotes: 'ifNeeded'})", map("file", fileName2), @@ -597,7 +602,7 @@ public void testConsistentQuotingIfNeeded() { assertEquals(EXPECTED_ALL_IF_NEEDED_REL, readFile(fileNameStart + ".relationships.REL.csv")); // Streaming - TestUtil.testCall(db, "CALL apoc.export.csv.all(null,{stream: true, quotes: 'ifNeeded'})", (r) -> { + testCall(db, "CALL apoc.export.csv.all(null,{stream: true, quotes: 'ifNeeded'})", (r) -> { String data = (String) r.get("data"); assertEquals(EXPECTED_ALL_IF_NEEDED, data); }); @@ -607,7 +612,7 @@ public void testConsistentQuotingIfNeeded() { public void testConsistentQuotingIfNeededDifferentiateNulls() { // All in one file String fileName1 = "allOneFileIfNeeded.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: false, quotes: 'ifNeeded', differentiateNulls: true})", map("file", fileName1), @@ -617,7 +622,7 @@ public void testConsistentQuotingIfNeededDifferentiateNulls() { // In separate files String fileNameStart = "allBulkImportIfNeeded"; String fileName2 = fileNameStart + ".csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: true, quotes: 'ifNeeded', differentiateNulls: true})", map("file", fileName2), @@ -636,7 +641,7 @@ public void testConsistentQuotingIfNeededDifferentiateNulls() { EXPECTED_ALL_DIFFERENTIATE_NULLS_IF_NEEDED_REL, readFile(fileNameStart + ".relationships.REL.csv")); // Streaming - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all(null,{stream: true, quotes: 'ifNeeded', differentiateNulls: true})", (r) -> { @@ -649,7 +654,7 @@ public void testConsistentQuotingIfNeededDifferentiateNulls() { public void testConsistentQuotingNone() { // All in one file String fileName1 = "allOneFileNone.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: false, quotes: 'none'})", map("file", fileName1), @@ -659,7 +664,7 @@ public void testConsistentQuotingNone() { // In separate files String fileNameStart = "allBulkImportIfNone"; String fileName2 = fileNameStart + ".csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{bulkImport: true, quotes: 'none'})", map("file", fileName2), @@ -675,7 +680,7 @@ public void testConsistentQuotingNone() { assertEquals(EXPECTED_ALL_IF_NEEDED_REL, readFile(fileNameStart + ".relationships.REL.csv")); // Streaming - TestUtil.testCall(db, "CALL apoc.export.csv.all(null,{stream: true, quotes: 'none'})", (r) -> { + testCall(db, "CALL apoc.export.csv.all(null,{stream: true, quotes: 'none'})", (r) -> { String data = (String) r.get("data"); assertEquals(EXPECTED_ALL_NONE, data); }); @@ -694,7 +699,7 @@ public void testCsvRoundTrip() { "MATCH (u:Roundtrip) return u.name as name", "config", map(CompressionConfig.COMPRESSION, GZIP.name())); - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.query($query, $file, $config)", params, @@ -703,7 +708,7 @@ public void testCsvRoundTrip() { final String deleteQuery = "MATCH (n:Roundtrip) DETACH DELETE n"; db.executeTransactionally(deleteQuery); - TestUtil.testCall( + testCall( db, "CALL apoc.import.csv([{fileName: $file, labels: ['Roundtrip']}], [], $config) ", params, @@ -725,13 +730,12 @@ public void testCsvBackslashes() { final Map params = map("file", fileName, "query", "MATCH (n: Test) RETURN n", "config", map("quotes", "always")); - TestUtil.testCall( - db, "CALL apoc.export.csv.all($file, $config)", params, (r) -> assertEquals(fileName, r.get("file"))); + testCall(db, "CALL apoc.export.csv.all($file, $config)", params, (r) -> assertEquals(fileName, r.get("file"))); final String deleteQuery = "MATCH (n:Test) DETACH DELETE n"; db.executeTransactionally(deleteQuery); - TestUtil.testCall( + testCall( db, "CALL apoc.import.csv([{fileName: $file, labels: ['Test']}],[],{})", params, @@ -770,7 +774,7 @@ public void testCsvQueryWithDifferentiatedNulls() { "config", map("quotes", quotingType, "differentiateNulls", shouldDifferentiateNulls)); - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.query(\"MATCH (d:ESCAPING) WITH d RETURN d.age as age, d.name as name\", $file, $config)", params, @@ -805,7 +809,7 @@ public void testCsvDataWithDifferentiatedNulls() { "config", map("quotes", quotingType, "differentiateNulls", shouldDifferentiateNulls)); - TestUtil.testCall( + testCall( db, """ MATCH (n:ESCAPING) @@ -846,7 +850,7 @@ public void testCsvGraphWithDifferentiatedNulls() { "config", map("quotes", quotingType, "differentiateNulls", shouldDifferentiateNulls)); - TestUtil.testCall( + testCall( db, """ CALL apoc.graph.fromCypher('MATCH (n:ESCAPING) RETURN n',{}, 'test',{description: "test graph"}) yield graph @@ -886,7 +890,7 @@ public void testCsvAllWithDifferentiatedNulls() { "config", map("quotes", quotingType, "differentiateNulls", shouldDifferentiateNulls)); - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file, $config)", params, @@ -916,7 +920,7 @@ public void testExportAllCsvWithoutExtension() { } private void testExportCsvAllCommon(String fileName) { - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,null)", map("file", fileName), @@ -933,7 +937,7 @@ public void testExportAllCsvWithSample() throws IOException { final long totalNodes = 14L; final long totalRels = 4L; final long totalProps = 29L; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file, null)", map("file", fileName), @@ -941,7 +945,7 @@ public void testExportAllCsvWithSample() throws IOException { assertEquals(EXP_SAMPLE, readFile(fileName)); // quotes: 'none' to simplify header testing - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file, {sampling: true, samplingConfig: {sample: 1}, quotes: 'none'})", map("file", fileName), @@ -960,7 +964,7 @@ public void testExportAllCsvWithSample() throws IOException { @Test public void testExportAllCsvWithQuotes() { String fileName = "all.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{quotes: true})", map("file", fileName), @@ -971,7 +975,7 @@ public void testExportAllCsvWithQuotes() { @Test public void testExportAllCsvWithoutQuotes() { String fileName = "all.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{quotes: 'none'})", map("file", fileName), @@ -982,7 +986,7 @@ public void testExportAllCsvWithoutQuotes() { @Test public void testExportAllCsvNeededQuotes() { String fileName = "all.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.all($file,{quotes: 'ifNeeded'})", map("file", fileName), @@ -993,7 +997,7 @@ public void testExportAllCsvNeededQuotes() { @Test public void testExportGraphCsv() { String fileName = "graph.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.graph.fromDB('test',{}) yield graph " + "CALL apoc.export.csv.graph(graph, $file,{quotes: 'none'}) " @@ -1007,7 +1011,7 @@ public void testExportGraphCsv() { @Test public void testExportGraphCsvWithoutQuotes() { String fileName = "graph.csv"; - TestUtil.testCall( + testCall( db, "CALL apoc.graph.fromDB('test',{}) yield graph " + "CALL apoc.export.csv.graph(graph, $file,null) " + "YIELD nodes, relationships, properties, file, source,format, time " @@ -1021,13 +1025,11 @@ public void testExportGraphCsvWithoutQuotes() { public void testExportQueryCsv() { String fileName = "query.csv"; String query = "MATCH (u:User) return u.age, u.name, u.male, u.kids, labels(u)"; - TestUtil.testCall( - db, "CALL apoc.export.csv.query($query,$file,null)", map("file", fileName, "query", query), (r) -> { - assertTrue( - "Should get statement", r.get("source").toString().contains("statement: cols(5)")); - assertEquals(fileName, r.get("file")); - assertEquals("csv", r.get("format")); - }); + testCall(db, "CALL apoc.export.csv.query($query,$file,null)", map("file", fileName, "query", query), (r) -> { + assertTrue("Should get statement", r.get("source").toString().contains("statement: cols(5)")); + assertEquals(fileName, r.get("file")); + assertEquals("csv", r.get("format")); + }); assertEquals(EXPECTED_QUERY, readFile(fileName)); } @@ -1035,7 +1037,7 @@ public void testExportQueryCsv() { public void testExportQueryCsvWithoutQuotes() { String fileName = "query.csv"; String query = "MATCH (u:User) return u.age, u.name, u.male, u.kids, labels(u)"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.query($query,$file,{quotes: false})", map("file", fileName, "query", query), @@ -1057,7 +1059,7 @@ public void testExportCsvAdminOperationErrorMessage() { for (String query : invalidQueries) { QueryExecutionException e = Assert.assertThrows( QueryExecutionException.class, - () -> TestUtil.testCall( + () -> testCall( db, """ CALL apoc.export.csv.query( @@ -1076,13 +1078,11 @@ public void testExportCsvAdminOperationErrorMessage() { public void testExportQueryNodesCsv() { String fileName = "query_nodes.csv"; String query = "MATCH (u:User) return u"; - TestUtil.testCall( - db, "CALL apoc.export.csv.query($query,$file,null)", map("file", fileName, "query", query), (r) -> { - assertTrue( - "Should get statement", r.get("source").toString().contains("statement: cols(1)")); - assertEquals(fileName, r.get("file")); - assertEquals("csv", r.get("format")); - }); + testCall(db, "CALL apoc.export.csv.query($query,$file,null)", map("file", fileName, "query", query), (r) -> { + assertTrue("Should get statement", r.get("source").toString().contains("statement: cols(1)")); + assertEquals(fileName, r.get("file")); + assertEquals("csv", r.get("format")); + }); assertEquals(EXPECTED_QUERY_NODES, readFile(fileName)); } @@ -1090,7 +1090,7 @@ public void testExportQueryNodesCsv() { public void testExportQueryNodesCsvParams() { String fileName = "query_nodes.csv"; String query = "MATCH (u:User) WHERE u.age > $age return u"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.query($query,$file,{params:{age:10}})", map("file", fileName, "query", query), @@ -1321,7 +1321,7 @@ public void testExportQueryCsvIssue1188() { db.executeTransactionally( "CREATE (n:Document{pk:$pk, copyright: $copyright})", map("copyright", copyright, "pk", pk)); String query = "MATCH (n:Document{pk:'5921569'}) return n.pk as pk, n.copyright as copyright"; - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.query($query, null, $config)", map("query", query, "config", map("stream", true)), @@ -1339,7 +1339,7 @@ public void testExportWgsPoint() { db.executeTransactionally( "CREATE (p:Position {place: point({latitude: 12.78, longitude: 56.7, height: 1.1})})"); - TestUtil.testCall( + testCall( db, "CALL apoc.export.csv.query($query, null, {quotes: 'none', stream: true}) YIELD data RETURN data", map("query", "MATCH (p:Position) RETURN p.place as place"), @@ -1377,4 +1377,14 @@ private Consumer getAndCheckStreamingMetadataQueryMatchAddress(StringBui sb.append(r.get("data")); }; } + + @Test + public void testDifferentCypherVersionsApocCsvQuery() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.export.csv.query('%s RETURN apoc.cypherVersion() AS version', null, { stream:true }) YIELD data RETURN data", + cypherVersion.outerVersion, cypherVersion.innerVersion); + testCall(db, query, r -> assertTrue(r.get("data").toString().contains(cypherVersion.result))); + } + } } diff --git a/core/src/test/java/apoc/export/cypher/ExportCypherTest.java b/core/src/test/java/apoc/export/cypher/ExportCypherTest.java index 0f88dfa34..6b46f25de 100644 --- a/core/src/test/java/apoc/export/cypher/ExportCypherTest.java +++ b/core/src/test/java/apoc/export/cypher/ExportCypherTest.java @@ -22,6 +22,7 @@ import static apoc.export.util.ExportFormat.*; import static apoc.util.BinaryTestUtil.getDecompressedData; import static apoc.util.TestUtil.assertError; +import static apoc.util.TestUtil.testCall; import static apoc.util.Util.INVALID_QUERY_MODE_ERROR; import static apoc.util.Util.map; import static java.nio.charset.StandardCharsets.UTF_8; @@ -32,6 +33,7 @@ import static org.neo4j.configuration.SettingImpl.newBuilder; import static org.neo4j.configuration.SettingValueParsers.BOOL; +import apoc.HelperProcedures; import apoc.export.util.ExportConfig; import apoc.util.BinaryTestUtil; import apoc.util.CompressionAlgo; @@ -49,12 +51,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; +import junit.framework.TestCase; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.cypher.export.DatabaseSubGraph; import org.neo4j.graphdb.QueryExecutionException; @@ -98,6 +102,7 @@ public class ExportCypherTest { .withSetting( GraphDatabaseSettings.load_csv_file_url_root, directory.toPath().toAbsolutePath()) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true) .withSetting( newBuilder("internal.dbms.debug.track_cursor_close", BOOL, false) .build(), @@ -2417,4 +2422,20 @@ public static String convertToCypherShellFormat(String input) { .replace(NEO4J_SHELL.schemaAwait(), CYPHER_SHELL.schemaAwait()); } } + + @Test + public void testDifferentCypherVersionsApocCypherQuery() { + db.executeTransactionally("CREATE (:Test {prop: 'CYPHER_5'}), (:Test {prop: 'CYPHER_25'})"); + + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.export.cypher.query('%s MATCH (n:Test {prop: apoc.cypherVersion() }) RETURN n LIMIT 1', null, { stream:true }) YIELD cypherStatements RETURN cypherStatements", + cypherVersion.outerVersion, cypherVersion.innerVersion); + testCall( + db, + query, + r -> TestCase.assertTrue( + r.get("cypherStatements").toString().contains(cypherVersion.result))); + } + } } diff --git a/core/src/test/java/apoc/export/cypher/ExportCypherTestUtils.java b/core/src/test/java/apoc/export/cypher/ExportCypherTestUtils.java index a26faf1c4..90f5ece52 100644 --- a/core/src/test/java/apoc/export/cypher/ExportCypherTestUtils.java +++ b/core/src/test/java/apoc/export/cypher/ExportCypherTestUtils.java @@ -21,6 +21,7 @@ import static apoc.ApocConfig.APOC_EXPORT_FILE_ENABLED; import static apoc.ApocConfig.apocConfig; +import apoc.HelperProcedures; import apoc.cypher.Cypher; import apoc.graph.Graphs; import apoc.schema.Schemas; @@ -36,7 +37,8 @@ public class ExportCypherTestUtils { public static void setUp(GraphDatabaseService db, TestName testName) { apocConfig().setProperty(APOC_EXPORT_FILE_ENABLED, true); - TestUtil.registerProcedure(db, ExportCypher.class, Graphs.class, Schemas.class, Cypher.class); + TestUtil.registerProcedure( + db, ExportCypher.class, Graphs.class, Schemas.class, Cypher.class, HelperProcedures.class); if (testName.getMethodName().contains(ROUND_TRIP)) return; db.executeTransactionally("CREATE RANGE INDEX barIndex FOR (n:Bar) ON (n.first_name, n.last_name)"); db.executeTransactionally("CREATE RANGE INDEX fooIndex FOR (n:Foo) ON (n.name)"); diff --git a/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java b/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java index 6d754300a..e582d41a1 100644 --- a/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java +++ b/core/src/test/java/apoc/export/graphml/ExportGraphMLTest.java @@ -24,6 +24,7 @@ import static apoc.util.BinaryTestUtil.getDecompressedData; import static apoc.util.MapUtil.map; import static apoc.util.TestUtil.assertError; +import static apoc.util.TestUtil.testCall; import static apoc.util.TestUtil.testResult; import static apoc.util.TransactionTestUtil.checkTerminationGuard; import static apoc.util.Util.INVALID_QUERY_MODE_ERROR; @@ -36,6 +37,7 @@ import static org.junit.Assert.fail; import static org.neo4j.graphdb.Label.label; +import apoc.HelperProcedures; import apoc.util.BinaryTestUtil; import apoc.util.CompressionAlgo; import apoc.util.CompressionConfig; @@ -59,6 +61,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestName; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.graphdb.Node; import org.neo4j.graphdb.QueryExecutionException; @@ -94,6 +97,7 @@ public class ExportGraphMLTest { @Rule public DbmsRule db = new ImpermanentDbmsRule() .withSetting(GraphDatabaseSettings.memory_tracking, true) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true) .withSetting( GraphDatabaseSettings.load_csv_file_url_root, directory.toPath().toAbsolutePath()); @@ -1101,4 +1105,17 @@ public void testExportGraphmlAdminOperationErrorMessage() { assertError(e, INVALID_QUERY_MODE_ERROR, RuntimeException.class, "apoc.export.graphml.query"); } } + + @Test + public void testDifferentCypherVersionsApocGraphmlQuery() { + db.executeTransactionally("CREATE (:Test {prop: 'CYPHER_5'}), (:Test {prop: 'CYPHER_25'})"); + + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.export.graphml.query('%s MATCH (n:Test {prop: apoc.cypherVersion() }) RETURN n LIMIT 1', null, { stream:true }) YIELD data RETURN data", + cypherVersion.outerVersion, cypherVersion.innerVersion); + testCall( + db, query, r -> TestCase.assertTrue(r.get("data").toString().contains(cypherVersion.result))); + } + } } diff --git a/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java b/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java index 5c266ff9a..88588d22b 100644 --- a/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java +++ b/core/src/test/java/apoc/export/graphml/ExportGraphMLTestUtil.java @@ -25,6 +25,7 @@ import static org.junit.Assert.assertFalse; import static org.xmlunit.diff.ElementSelectors.byName; +import apoc.HelperProcedures; import apoc.graph.Graphs; import apoc.util.TestUtil; import java.util.Arrays; @@ -237,7 +238,7 @@ public static void assertXMLEquals(Object output, String xmlString) { } public static void setUpGraphMl(GraphDatabaseService db, TestName testName) { - TestUtil.registerProcedure(db, ExportGraphML.class, Graphs.class); + TestUtil.registerProcedure(db, ExportGraphML.class, Graphs.class, HelperProcedures.class); apocConfig() .setProperty( diff --git a/core/src/test/java/apoc/export/json/ExportJsonTest.java b/core/src/test/java/apoc/export/json/ExportJsonTest.java index 2940bed57..833455fad 100644 --- a/core/src/test/java/apoc/export/json/ExportJsonTest.java +++ b/core/src/test/java/apoc/export/json/ExportJsonTest.java @@ -29,6 +29,7 @@ import static apoc.util.CompressionConfig.COMPRESSION; import static apoc.util.MapUtil.map; import static apoc.util.TestUtil.assertError; +import static apoc.util.TestUtil.testCall; import static apoc.util.Util.INVALID_QUERY_MODE_ERROR; import static java.nio.charset.StandardCharsets.UTF_8; import static org.junit.Assert.assertEquals; @@ -37,6 +38,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import apoc.HelperProcedures; import apoc.graph.Graphs; import apoc.util.BinaryTestUtil; import apoc.util.CompressionAlgo; @@ -48,11 +50,13 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import junit.framework.TestCase; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.graphdb.Label; import org.neo4j.graphdb.Node; @@ -75,11 +79,12 @@ public class ExportJsonTest { public DbmsRule db = new ImpermanentDbmsRule() .withSetting( GraphDatabaseSettings.load_csv_file_url_root, - directory.toPath().toAbsolutePath()); + directory.toPath().toAbsolutePath()) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true); @Before public void setup() { - TestUtil.registerProcedure(db, ExportJson.class, ImportJson.class, Graphs.class); + TestUtil.registerProcedure(db, ExportJson.class, ImportJson.class, Graphs.class, HelperProcedures.class); apocConfig().setProperty(APOC_IMPORT_FILE_ENABLED, true); apocConfig().setProperty(APOC_EXPORT_FILE_ENABLED, true); db.executeTransactionally( @@ -754,4 +759,15 @@ private void assertStreamResults(Map r, final String source) { private void assertStreamEquals(String fileName, String actualText) { FileTestUtil.assertStreamEquals(directoryExpected, fileName, actualText); } + + @Test + public void testDifferentCypherVersionsApocJsonQuery() { + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.export.json.query('%s RETURN apoc.cypherVersion() AS version', null, { stream:true }) YIELD data RETURN data", + cypherVersion.outerVersion, cypherVersion.innerVersion); + testCall( + db, query, r -> TestCase.assertTrue(r.get("data").toString().contains(cypherVersion.result))); + } + } } diff --git a/core/src/test/java/apoc/graph/GraphsTest.java b/core/src/test/java/apoc/graph/GraphsTest.java index b84f72406..2bc39947a 100644 --- a/core/src/test/java/apoc/graph/GraphsTest.java +++ b/core/src/test/java/apoc/graph/GraphsTest.java @@ -19,6 +19,7 @@ package apoc.graph; import static apoc.util.MapUtil.map; +import static apoc.util.TestUtil.testCall; import static java.util.Arrays.asList; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertArrayEquals; @@ -26,7 +27,9 @@ import static org.junit.Assert.assertFalse; import static org.neo4j.graphdb.Label.label; +import apoc.HelperProcedures; import apoc.graph.util.GraphsConfig; +import apoc.nodes.Nodes; import apoc.util.JsonUtil; import apoc.util.TestUtil; import apoc.util.Util; @@ -51,6 +54,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.neo4j.configuration.GraphDatabaseInternalSettings; +import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.configuration.SettingImpl; import org.neo4j.configuration.SettingValueParsers; import org.neo4j.graphdb.Entity; @@ -80,6 +85,8 @@ boolean virtual(Entity entity) { @Rule public DbmsRule db = new ImpermanentDbmsRule() + .withSetting(GraphDatabaseSettings.procedure_unrestricted, Collections.singletonList("apoc.*")) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true) .withSetting( SettingImpl.newBuilder("internal.dbms.debug.track_cursor_close", SettingValueParsers.BOOL, false) .build(), @@ -91,7 +98,7 @@ boolean virtual(Entity entity) { @Before public void setUp() { - TestUtil.registerProcedure(db, Graphs.class); + TestUtil.registerProcedure(db, Graphs.class, Nodes.class, HelperProcedures.class); db.executeTransactionally( "CREATE (a:Actor {name:'Tom Hanks'})-[r:ACTED_IN {roles:'Forrest'}]->(m:Movie {title:'Forrest Gump'}) RETURN [a,m] as nodes, [r] as relationships", Collections.emptyMap(), @@ -1354,4 +1361,16 @@ public void testValidationCustomIdAsProperties() { assertEquals(1, relMap.get("(Tweet)-[USER]-(User)").size()); }); } + + @Test + public void testDifferentCypherVersionsApocCsvQuery() { + db.executeTransactionally("CREATE (:Test {prop: 'CYPHER_5'}), (:Test {prop: 'CYPHER_25'})"); + + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.graph.fromCypher('%s MATCH (n:Test {prop: apoc.cypherVersion() }) RETURN n LIMIT 1', {}, 'gem', {}) YIELD graph RETURN apoc.any.property(graph.nodes[0], 'prop') AS version", + cypherVersion.outerVersion, cypherVersion.innerVersion); + testCall(db, query, r -> assertEquals(cypherVersion.result, r.get("version"))); + } + } } diff --git a/core/src/test/java/apoc/meta/MetaTest.java b/core/src/test/java/apoc/meta/MetaTest.java index 5913870e9..015779091 100644 --- a/core/src/test/java/apoc/meta/MetaTest.java +++ b/core/src/test/java/apoc/meta/MetaTest.java @@ -36,7 +36,9 @@ import static org.neo4j.driver.Values.isoDuration; import static org.neo4j.graphdb.traversal.Evaluators.toDepth; +import apoc.HelperProcedures; import apoc.graph.Graphs; +import apoc.nodes.Nodes; import apoc.util.MapUtil; import apoc.util.TestUtil; import apoc.util.Util; @@ -58,6 +60,7 @@ import org.junit.Rule; import org.junit.Test; import org.mockito.Mockito; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.graphdb.*; import org.neo4j.test.rule.DbmsRule; @@ -75,6 +78,7 @@ public class MetaTest { @Rule public DbmsRule db = new ImpermanentDbmsRule() .withSetting(GraphDatabaseSettings.procedure_unrestricted, singletonList("apoc.*")) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true) .withSetting( newBuilder("internal.dbms.debug.track_cursor_close", BOOL, false) .build(), @@ -84,7 +88,7 @@ public class MetaTest { @Before public void setUp() { - TestUtil.registerProcedure(db, Meta.class, Graphs.class); + TestUtil.registerProcedure(db, Meta.class, Graphs.class, Nodes.class, HelperProcedures.class); } @After @@ -2343,4 +2347,29 @@ public void testMetaGraphSparseSampling() { db.executeTransactionally("MATCH (n) DETACH DELETE n"); } + + @Test + public void testDifferentCypherVersionsApocMetaDataOf() { + db.executeTransactionally("CREATE (:CYPHER_5 {prop: 1}), (:CYPHER_25 {prop: 1})"); + + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.meta.data.of('%s MATCH (n:$(apoc.cypherVersion())) RETURN n') YIELD label RETURN label", + cypherVersion.outerVersion, cypherVersion.innerVersion); + testCall(db, query, r -> assertEquals(cypherVersion.result, r.get("label"))); + } + } + + @Test + public void testDifferentCypherVersionsApocMetaGraphOf() { + db.executeTransactionally("CREATE (:CYPHER_5), (:CYPHER_25)"); + + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.meta.graph.of('%s MATCH (n:$(apoc.cypherVersion())) RETURN n') YIELD nodes RETURN labels(nodes[0])[0] AS version", + cypherVersion.outerVersion, cypherVersion.innerVersion); + + testCall(db, query, r -> assertEquals(cypherVersion.result, r.get("version"))); + } + } } diff --git a/core/src/test/java/apoc/periodic/PeriodicTest.java b/core/src/test/java/apoc/periodic/PeriodicTest.java index 5bc2bbaa5..7e0196a2c 100644 --- a/core/src/test/java/apoc/periodic/PeriodicTest.java +++ b/core/src/test/java/apoc/periodic/PeriodicTest.java @@ -18,7 +18,6 @@ */ package apoc.periodic; -import static apoc.periodic.Periodic.applyPlanner; import static apoc.util.TestUtil.testCall; import static apoc.util.TestUtil.testResult; import static apoc.util.TransactionTestUtil.lastTransactionChecks; @@ -37,11 +36,13 @@ import static org.neo4j.driver.internal.util.Iterables.count; import static org.neo4j.test.assertion.Assert.assertEventually; +import apoc.HelperProcedures; import apoc.cypher.Cypher; import apoc.refactor.GraphRefactoring; import apoc.schema.Schemas; import apoc.util.MapUtil; import apoc.util.TestUtil; +import apoc.util.Util; import apoc.util.Utils; import apoc.util.collection.Iterators; import java.util.Collections; @@ -59,6 +60,7 @@ import org.junit.Rule; import org.junit.Test; import org.neo4j.common.DependencyResolver; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.graphdb.QueryExecutionException; import org.neo4j.graphdb.Result; import org.neo4j.graphdb.Transaction; @@ -94,12 +96,20 @@ public void mockLog(@Name("value") String value) { public static AssertableLogProvider logProvider = new AssertableLogProvider(); @Rule - public DbmsRule db = new ImpermanentDbmsRule(logProvider); + public DbmsRule db = new ImpermanentDbmsRule(logProvider) + .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true); @Before public void initDb() { TestUtil.registerProcedure( - db, Periodic.class, Schemas.class, Cypher.class, Utils.class, MockLogger.class, GraphRefactoring.class); + db, + Periodic.class, + Schemas.class, + Cypher.class, + Utils.class, + MockLogger.class, + GraphRefactoring.class, + HelperProcedures.class); db.executeTransactionally( "call apoc.periodic.list() yield name call apoc.periodic.cancel(name) yield name as name2 return count(*)"); } @@ -238,42 +248,60 @@ public void testSubmitStatementWithParams() throws Exception { @Test public void testApplyPlanner() { - assertEquals("RETURN 1", applyPlanner("RETURN 1", Periodic.Planner.DEFAULT)); + assertEquals("CYPHER 5 RETURN 1", Util.applyPlanner("RETURN 1", Util.Planner.DEFAULT, "5")); + assertEquals( + "CYPHER 5 planner=cost MATCH (n:cypher) RETURN n", + Util.applyPlanner("MATCH (n:cypher) RETURN n", Util.Planner.COST, "5")); assertEquals( - "cypher planner=cost MATCH (n:cypher) RETURN n", - applyPlanner("MATCH (n:cypher) RETURN n", Periodic.Planner.COST)); + "CYPHER 25 planner=cost MATCH (n:cypher) RETURN n", + Util.applyPlanner("CYPHER 25 MATCH (n:cypher) RETURN n", Util.Planner.COST, "5")); assertEquals( - "cypher planner=idp MATCH (n:cypher) RETURN n", - applyPlanner("MATCH (n:cypher) RETURN n", Periodic.Planner.IDP)); + "CYPHER 5 planner=idp MATCH (n:cypher) RETURN n", + Util.applyPlanner("MATCH (n:cypher) RETURN n", Util.Planner.IDP, "5")); assertEquals( - "cypher planner=dp runtime=compiled MATCH (n) RETURN n", - applyPlanner("cypher runtime=compiled MATCH (n) RETURN n", Periodic.Planner.DP)); - assertEquals("cypher planner=dp MATCH (n) RETURN n", applyPlanner("MATCH (n) RETURN n", Periodic.Planner.DP)); + "CYPHER 25 planner=dp runtime=compiled MATCH (n) RETURN n", + Util.applyPlanner("cypher runtime=compiled MATCH (n) RETURN n", Util.Planner.DP, "25")); assertEquals( - "cypher planner=idp expressionEngine=compiled MATCH (n) RETURN n", - applyPlanner("cypher expressionEngine=compiled MATCH (n) RETURN n", Periodic.Planner.IDP)); + "CYPHER 5 planner=dp MATCH (n) RETURN n", + Util.applyPlanner("MATCH (n) RETURN n", Util.Planner.DP, "5")); assertEquals( - "cypher expressionEngine=compiled planner=cost MATCH (n) RETURN n", - applyPlanner("cypher expressionEngine=compiled planner=idp MATCH (n) RETURN n", Periodic.Planner.COST)); + "CYPHER 25 planner=idp expressionEngine=compiled MATCH (n) RETURN n", + Util.applyPlanner("cypher expressionEngine=compiled MATCH (n) RETURN n", Util.Planner.IDP, "25")); assertEquals( - "cypher planner=cost MATCH (n) RETURN n", - applyPlanner("cypher planner=cost MATCH (n) RETURN n", Periodic.Planner.COST)); + "CYPHER 5 expressionEngine=compiled planner=cost MATCH (n) RETURN n", + Util.applyPlanner( + "cypher expressionEngine=compiled planner=idp MATCH (n) RETURN n", Util.Planner.COST, "5")); + assertEquals( + "CYPHER 5 planner=cost MATCH (n) RETURN n", + Util.applyPlanner("cypher planner=cost MATCH (n) RETURN n", Util.Planner.COST, "5")); } @Test public void testSlottedRuntime() { + // Positive Tests + assertEquals( + "CYPHER 5 runtime=slotted MATCH (n:cypher) RETURN n", + Util.slottedRuntime("MATCH (n:cypher) RETURN n", "5")); + assertEquals("CYPHER 25 runtime=slotted MATCH (n) RETURN n", Util.slottedRuntime("MATCH (n) RETURN n", "25")); assertEquals( - "cypher runtime=slotted MATCH (n:cypher) RETURN n", - Periodic.slottedRuntime("MATCH (n:cypher) RETURN n")); - assertTrue(Periodic.slottedRuntime("MATCH (n) RETURN n").contains("cypher runtime=slotted ")); - assertFalse(Periodic.slottedRuntime(" cypher runtime=compiled MATCH (n) RETURN n") - .contains("cypher runtime=slotted ")); - assertFalse(Periodic.slottedRuntime("cypher runtime=compiled MATCH (n) RETURN n") - .contains("cypher runtime=slotted cypher")); - assertTrue(Periodic.slottedRuntime(" MATCH (n) RETURN n").contains(" runtime=slotted ")); - assertTrue(Periodic.slottedRuntime("cypher expressionEngine=compiled MATCH (n) RETURN n") - .contains(" runtime=slotted ")); - assertFalse(Periodic.slottedRuntime("cypher expressionEngine=compiled MATCH (n) RETURN n") + "CYPHER 5 runtime=slotted MATCH (n) RETURN n", + Util.slottedRuntime("CYPHER 5 MATCH (n) RETURN n", "25")); + assertEquals("CYPHER 5 runtime=slotted MATCH (n) RETURN n", Util.slottedRuntime(" MATCH (n) RETURN n", "5")); + assertEquals( + "CYPHER 5 runtime=slotted expressionEngine=compiled MATCH (n) RETURN n", + Util.slottedRuntime("cypher expressionEngine=compiled MATCH (n) RETURN n", "5")); + assertEquals( + "CYPHER 5 runtime=compiled expressionEngine=compiled MATCH (n) RETURN n", + Util.slottedRuntime("CYPHER 5 runtime=compiled expressionEngine=compiled MATCH (n) RETURN n", "5")); + + // Negative tests + assertFalse(Util.slottedRuntime(" cypher runtime=compiled MATCH (n) RETURN n", "5") + .contains("runtime=slotted ")); + assertFalse(Util.slottedRuntime("cypher runtime=compiled MATCH (n) RETURN n", "25") + .contains("runtime=slotted cypher")); + assertFalse(Util.slottedRuntime("cypher expressionEngine=compiled MATCH (n) RETURN n", "5") + .contains(" runtime=slotted cypher")); + assertFalse(Util.slottedRuntime("cypher 25 expressionEngine=compiled MATCH (n) RETURN n", "5") .contains(" runtime=slotted cypher")); } @@ -328,7 +356,7 @@ public void testPeriodicIterateErrors() { assertEquals(10L, row.get("failedBatches")); String expectedPattern = - "(?s)Invalid input.*\\\"UNWIND \\$_batch AS _batch WITH _batch.id AS id CREATE null\\\".*"; + "(?s)Invalid input.*\\\"CYPHER(?:\\s+(\\d+))? UNWIND \\$_batch AS _batch WITH _batch.id AS id CREATE null\\\".*"; String expectedBatchPattern = "org.neo4j.graphdb.QueryExecutionException: " + expectedPattern; @@ -928,4 +956,89 @@ private void testCypherFail(String query) { () -> testCall(db, query, row -> fail("The test should fail but it didn't"))); assertTrue(ExceptionUtils.getRootCause(e) instanceof org.neo4j.exceptions.SyntaxException); } + + @Test + public void testDifferentCypherVersionsApocPeriodicCommit() { + int id = 0; + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.periodic.commit('%s CREATE (n:$(apoc.cypherVersion()) {id: %d}) RETURN 0 LIMIT 1')", + cypherVersion.outerVersion, cypherVersion.innerVersion, id); + db.executeTransactionally(query); + // Check the node was created with the right label + var checkerQuery = + String.format("MATCH (n:%s {id : %d}) RETURN count(n) AS count", cypherVersion.result, id); + testCall(db, checkerQuery, r -> assertEquals(1L, r.get("count"))); + id++; + } + } + + @Test + public void testDifferentCypherVersionsApocPeriodicCountdown() throws InterruptedException { + int id = 0; + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.periodic.countdown('test', '%s CREATE (n:$(apoc.cypherVersion()) {id: %d}) RETURN 0 LIMIT 1', 0)", + cypherVersion.outerVersion, cypherVersion.innerVersion, id); + db.executeTransactionally(query); + Thread.sleep(2000); // Wait 3s to make sure the countdown has been called + // Check the node was created with the right label + var checkerQuery = + String.format("MATCH (n:%s {id : %d}) RETURN count(n) AS count", cypherVersion.result, id); + testCall(db, checkerQuery, r -> assertEquals(1L, r.get("count"))); + id++; + } + } + + @Test + public void testDifferentCypherVersionsApocPeriodicIterate() { + db.executeTransactionally("CREATE (:CYPHER_5 {id: -1}), (:CYPHER_25 {id: -1})"); + int id = 0; + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.periodic.iterate('%s MATCH (p:$(apoc.cypherVersion())) RETURN p', 'SET p.id = %d', {})", + cypherVersion.outerVersion, cypherVersion.innerVersion, id); + db.executeTransactionally(query); + // Check the node was created with the right label + var checkerQuery = + String.format("MATCH (n:%s {id: %d}) RETURN count(n) AS count", cypherVersion.result, id); + testCall(db, checkerQuery, r -> assertEquals(1L, r.get("count"))); + id++; + } + } + + @Test + public void testDifferentCypherVersionsApocPeriodicRepeat() throws InterruptedException { + int id = 0; + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.periodic.repeat('test%d', '%s MERGE (n:$(apoc.cypherVersion()) {id: %d})', 1)", + cypherVersion.outerVersion, id, cypherVersion.innerVersion, id); + db.executeTransactionally(query); + Thread.sleep(3000); // Wait 3s to make sure the repeat has been called + db.executeTransactionally(String.format("CALL apoc.periodic.cancel('test%d')", id)); + // Check the node was created with the right label + var checkerQuery = + String.format("MATCH (n:%s {id : %d}) RETURN count(n) AS count", cypherVersion.result, id); + testCall(db, checkerQuery, r -> assertEquals(1L, r.get("count"))); + id++; + } + } + + @Test + public void testDifferentCypherVersionsApocPeriodicSubmit() throws InterruptedException { + int id = 0; + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + var query = String.format( + "%s CALL apoc.periodic.submit('test%d', '%s CREATE (n:$(apoc.cypherVersion()) {id: %d})')", + cypherVersion.outerVersion, id, cypherVersion.innerVersion, id); + db.executeTransactionally(query); + Thread.sleep(1000); // Wait 1s to make sure the submit has been called + // Check the node was created with the right label + var checkerQuery = + String.format("MATCH (n:%s {id : %d}) RETURN count(n) AS count", cypherVersion.result, id); + testCall(db, checkerQuery, r -> assertEquals(1L, r.get("count"))); + id++; + } + } } diff --git a/core/src/test/java/apoc/trigger/TriggerNewProceduresTest.java b/core/src/test/java/apoc/trigger/TriggerNewProceduresTest.java index 99b5de2a7..a824c8541 100644 --- a/core/src/test/java/apoc/trigger/TriggerNewProceduresTest.java +++ b/core/src/test/java/apoc/trigger/TriggerNewProceduresTest.java @@ -29,6 +29,7 @@ import static org.neo4j.internal.helpers.collection.MapUtil.map; import static org.neo4j.test.assertion.Assert.assertEventually; +import apoc.HelperProcedures; import apoc.nodes.Nodes; import apoc.util.TestUtil; import apoc.util.Util; @@ -45,6 +46,7 @@ import org.junit.Test; import org.junit.contrib.java.lang.system.ProvideSystemProperty; import org.junit.rules.TemporaryFolder; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.configuration.GraphDatabaseSettings; import org.neo4j.dbms.api.DatabaseManagementService; import org.neo4j.graphdb.GraphDatabaseService; @@ -83,6 +85,7 @@ public static void beforeClass() { databaseManagementService = new TestDatabaseManagementServiceBuilder( storeDir.getRoot().toPath()) .setConfig(procedure_unrestricted, List.of("apoc*")) + .setConfig(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true) .build(); db = databaseManagementService.database(GraphDatabaseSettings.DEFAULT_DATABASE_NAME); sysDb = databaseManagementService.database(GraphDatabaseSettings.SYSTEM_DATABASE_NAME); @@ -119,7 +122,7 @@ public void testListTriggers() { "CALL apoc.trigger.list", row -> { assertEquals("count-removals", row.get("name")); - assertEquals(query, row.get("query")); + assertTrue(row.get("query").toString().contains(query)); assertEquals(true, row.get("installed")); }, TIMEOUT); @@ -169,7 +172,7 @@ public void testOverwriteTrigger() { map("name", name, "query", queryOne), r -> { assertEquals(name, r.get("name")); - assertEquals(queryOne, r.get("query")); + assertTrue(r.get("query").toString().contains(queryOne)); }); String queryTwo = "RETURN 999"; @@ -179,7 +182,7 @@ public void testOverwriteTrigger() { map("name", name, "query", queryTwo), r -> { assertEquals(name, r.get("name")); - assertEquals(queryTwo, r.get("query")); + assertTrue(r.get("query").toString().contains(queryTwo)); }); } @@ -221,14 +224,14 @@ public void testRemoveTrigger() { "CALL apoc.trigger.list()", (row) -> { assertEquals("to-be-removed", row.get("name")); - assertEquals("RETURN 1", row.get("query")); + assertTrue(row.get("query").toString().contains("RETURN 1")); assertEquals(true, row.get("installed")); }, TIMEOUT); testCall(sysDb, "CALL apoc.trigger.drop('neo4j', 'to-be-removed')", (row) -> { assertEquals("to-be-removed", row.get("name")); - assertEquals("RETURN 1", row.get("query")); + assertTrue(row.get("query").toString().contains("RETURN 1")); assertEquals(false, row.get("installed")); }); testCallCountEventually(db, "CALL apoc.trigger.list()", 0, TIMEOUT); @@ -249,11 +252,11 @@ public void testRemoveAllTrigger() { TestUtil.testResult(sysDb, "CALL apoc.trigger.dropAll('neo4j')", (res) -> { Map row = res.next(); assertEquals("to-be-removed-1", row.get("name")); - assertEquals("RETURN 1", row.get("query")); + assertTrue(row.get("query").toString().contains("RETURN 1")); assertEquals(false, row.get("installed")); row = res.next(); assertEquals("to-be-removed-2", row.get("name")); - assertEquals("RETURN 2", row.get("query")); + assertTrue(row.get("query").toString().contains("RETURN 2")); assertEquals(false, row.get("installed")); assertFalse(res.hasNext()); }); @@ -713,10 +716,10 @@ public void testTriggerShow() { res -> { Map row = res.next(); assertEquals(name, row.get("name")); - assertEquals(query, row.get("query")); + assertTrue(row.get("query").toString().contains(query)); row = res.next(); assertEquals(name2, row.get("name")); - assertEquals(query, row.get("query")); + assertTrue(row.get("query").toString().contains(query)); assertFalse(res.hasNext()); }); } @@ -794,4 +797,32 @@ public void testEventualConsistency() { "CALL apoc.trigger.install('neo4j', $name, 'return 1', {})", map("name", UUID.randomUUID().toString())); } + + @Test + public void testCypherVersions() { + int id = 0; + for (HelperProcedures.CypherVersionCombinations cypherVersion : HelperProcedures.cypherVersions) { + String name = "cypher-versions-" + id; + String triggerQuery = + "MATCH (c:Counter) SET c.count = c.count + size([f IN $deletedNodes WHERE id(f) > 0])"; + + var query = String.format( + "%s CALL apoc.trigger.install('neo4j', $name, $query,{})", cypherVersion.outerVersion); + sysDb.executeTransactionally( + query, map("name", name, "query", cypherVersion.innerVersion + " " + triggerQuery)); + + // Check the query got given the correct Cypher Version + testCallEventually( + db, + "CALL apoc.trigger.list() YIELD name, query, installed WHERE name = $name RETURN *", + map("name", name), + row -> { + assertTrue(row.get("query").toString().contains(triggerQuery)); + assertTrue(row.get("query").toString().contains(cypherVersion.result.replace('_', ' '))); + assertEquals(true, row.get("installed")); + }, + 5L); + id++; + } + } } diff --git a/core/src/test/java/apoc/trigger/TriggerTest.java b/core/src/test/java/apoc/trigger/TriggerTest.java index be83d32b8..9e4eaf4a8 100644 --- a/core/src/test/java/apoc/trigger/TriggerTest.java +++ b/core/src/test/java/apoc/trigger/TriggerTest.java @@ -96,7 +96,7 @@ public void testListTriggers() { db, "CALL apoc.trigger.add('count-removals',$query,{}) YIELD name RETURN name", map("query", query), 1); TestUtil.testCall(db, "CALL apoc.trigger.list()", (row) -> { assertEquals("count-removals", row.get("name")); - assertEquals(query, row.get("query")); + assertTrue(row.get("query").toString().contains(query)); assertEquals(true, row.get("installed")); }); } @@ -140,12 +140,12 @@ public void testRemoveTrigger() { TestUtil.testCallCount(db, "CALL apoc.trigger.add('to-be-removed','RETURN 1',{}) YIELD name RETURN name", 1); TestUtil.testCall(db, "CALL apoc.trigger.list()", (row) -> { assertEquals("to-be-removed", row.get("name")); - assertEquals("RETURN 1", row.get("query")); + assertTrue(row.get("query").toString().contains("RETURN 1")); assertEquals(true, row.get("installed")); }); TestUtil.testCall(db, "CALL apoc.trigger.remove('to-be-removed')", (row) -> { assertEquals("to-be-removed", row.get("name")); - assertEquals("RETURN 1", row.get("query")); + assertTrue(row.get("query").toString().contains("RETURN 1")); assertEquals(false, row.get("installed")); }); @@ -169,10 +169,11 @@ public void testRemoveAllTrigger() { .toList(); assertEquals(2, rows.size()); assertEquals("to-be-removed-1", rows.get(0).get("name")); - assertEquals("RETURN 1", rows.get(0).get("query")); + assertTrue(rows.get(0).get("query").toString().contains("RETURN 1")); + assertEquals(false, rows.get(0).get("installed")); assertEquals("to-be-removed-2", rows.get(1).get("name")); - assertEquals("RETURN 2", rows.get(1).get("query")); + assertTrue(rows.get(1).get("query").toString().contains("RETURN 2")); assertEquals(false, rows.get(1).get("installed")); }); TestUtil.testCallCount(db, "CALL apoc.trigger.list()", 0); diff --git a/test-utils/src/main/java/apoc/trigger/TriggerTestUtil.java b/test-utils/src/main/java/apoc/trigger/TriggerTestUtil.java index 4e20f040c..2a7ca797e 100644 --- a/test-utils/src/main/java/apoc/trigger/TriggerTestUtil.java +++ b/test-utils/src/main/java/apoc/trigger/TriggerTestUtil.java @@ -20,6 +20,7 @@ import static apoc.util.TestUtil.testCallEventually; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import java.util.Map; import org.neo4j.graphdb.GraphDatabaseService; @@ -38,7 +39,7 @@ public static void awaitTriggerDiscovered(GraphDatabaseService db, String name, "CALL apoc.trigger.list() YIELD name, query, paused WHERE name = $name RETURN query, paused", Map.of("name", name), row -> { - assertEquals(query, row.get("query")); + assertTrue(row.get("query").toString().contains(query)); assertEquals(paused, row.get("paused")); }, TIMEOUT);