From 4449e2d6b166584f0bc13b6051fdeae838bd097f Mon Sep 17 00:00:00 2001 From: Gemma Lamont Date: Mon, 3 Feb 2025 07:53:17 +0100 Subject: [PATCH] Add tests to update procs/funcs --- common/src/main/java/apoc/util/Util.java | 11 +- .../main/java/apoc/export/csv/ExportCSV.java | 3 +- core/src/main/java/apoc/meta/Meta.java | 6 +- core/src/test/java/apoc/HelperProcedures.java | 39 +++ .../src/test/java/apoc/cypher/CypherTest.java | 224 +++++++++++++++++- .../java/apoc/export/csv/ExportCsvTest.java | 116 ++++----- .../apoc/export/cypher/ExportCypherTest.java | 21 ++ .../export/cypher/ExportCypherTestUtils.java | 4 +- .../export/graphml/ExportGraphMLTest.java | 17 ++ .../export/graphml/ExportGraphMLTestUtil.java | 3 +- .../java/apoc/export/json/ExportJsonTest.java | 20 +- core/src/test/java/apoc/graph/GraphsTest.java | 21 +- core/src/test/java/apoc/meta/MetaTest.java | 31 ++- .../test/java/apoc/periodic/PeriodicTest.java | 100 +++++++- .../trigger/TriggerNewProceduresTest.java | 49 +++- .../java/apoc/trigger/TriggerTestUtil.java | 3 +- 16 files changed, 590 insertions(+), 78 deletions(-) create mode 100644 core/src/test/java/apoc/HelperProcedures.java diff --git a/common/src/main/java/apoc/util/Util.java b/common/src/main/java/apoc/util/Util.java index 077be929f..7a1ce33ad 100644 --- a/common/src/main/java/apoc/util/Util.java +++ b/common/src/main/java/apoc/util/Util.java @@ -1381,7 +1381,7 @@ public static String getCypherVersionString(ProcedureCallContext procedureCallCo } private static final Pattern CYPHER_VERSION_PATTERN = - Pattern.compile("^(CYPHER)(?:\\s+(\\d+))?", Pattern.CASE_INSENSITIVE); + 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); @@ -1463,6 +1463,15 @@ public static String applyPlanner(String query, Planner planner, String cypherVe 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(), "")) { diff --git a/core/src/main/java/apoc/export/csv/ExportCSV.java b/core/src/main/java/apoc/export/csv/ExportCSV.java index 923fed248..ef174fd90 100644 --- a/core/src/main/java/apoc/export/csv/ExportCSV.java +++ b/core/src/main/java/apoc/export/csv/ExportCSV.java @@ -195,7 +195,8 @@ public Stream query( : (Map) config.getOrDefault("params", Collections.emptyMap()); final String source; - try (final var result = tx.execute(Util.prefixQueryWithCheck(procedureCallContext, query), params)) { + 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/meta/Meta.java b/core/src/main/java/apoc/meta/Meta.java index d14c4321f..ed20d3ebc 100644 --- a/core/src/main/java/apoc/meta/Meta.java +++ b/core/src/main/java/apoc/meta/Meta.java @@ -1137,9 +1137,9 @@ public Stream graphOf( Map config) { MetaConfig metaConfig = new MetaConfig(config, false); final SubGraph subGraph; - if (graph instanceof String) { - Result result = - tx.execute(Util.getCypherVersionPrefix(procedureCallContext) + " runtime=pipelined " + 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/test/java/apoc/HelperProcedures.java b/core/src/test/java/apoc/HelperProcedures.java new file mode 100644 index 000000000..4f0925c8b --- /dev/null +++ b/core/src/test/java/apoc/HelperProcedures.java @@ -0,0 +1,39 @@ +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 d222d7e2d..fede58b09 100644 --- a/core/src/test/java/apoc/periodic/PeriodicTest.java +++ b/core/src/test/java/apoc/periodic/PeriodicTest.java @@ -36,6 +36,7 @@ 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; @@ -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(*)"); } @@ -346,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; @@ -946,4 +956,88 @@ 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() { + 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); + // 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/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);