diff --git a/extended-it/src/test/java/apoc/neo4j/docker/ImportExportEnterpriseTest.java b/extended-it/src/test/java/apoc/neo4j/docker/ImportExportEnterpriseTest.java new file mode 100644 index 0000000000..f1730d0489 --- /dev/null +++ b/extended-it/src/test/java/apoc/neo4j/docker/ImportExportEnterpriseTest.java @@ -0,0 +1,96 @@ +package apoc.neo4j.docker; + +import apoc.util.Neo4jContainerExtension; +import apoc.util.TestContainerUtil; +import org.junit.*; +import org.neo4j.driver.Session; +import org.neo4j.driver.summary.ResultSummary; + +import java.util.List; +import java.util.Map; + +import static apoc.util.TestContainerUtil.createEnterpriseDB; +import static apoc.util.TestContainerUtil.testResult; + +public class ImportExportEnterpriseTest { + private static Neo4jContainerExtension neo4jContainer; + private static Session session; + + @BeforeClass + public static void beforeAll() throws InterruptedException { + neo4jContainer = createEnterpriseDB(List.of(TestContainerUtil.ApocPackage.EXTENDED, TestContainerUtil.ApocPackage.CORE), true) + .withNeo4jConfig("apoc.import.file.enabled", "true") + .withNeo4jConfig("apoc.export.file.enabled", "true") + .withNeo4jConfig("internal.cypher.enable_vector_type", "true"); + neo4jContainer.start(); + session = neo4jContainer.getSession(); + + + + } + + @AfterClass + public static void afterAll() { + neo4jContainer.close(); + } + + @Before + public void before() { + var vectorTypes = List.of("INT64", "INT32", "INT16", "INT8", "FLOAT64", "FLOAT32"); + for (String type : vectorTypes) { + session.executeWrite( + tx -> tx.run("CYPHER 25 CREATE (:VectorFoo { z: VECTOR([1, 2, 3], 3, %s) });".formatted(type)).consume() + ); + } + } + + @After + public void after() { + session.executeWrite(tx -> tx.run("MATCH (n:VectorFoo) DETACH DELETE n").consume()); + } + + @Test + public void testParquet() { + ResultSummary resultSummary = session.executeWrite(tx -> tx.run("CALL apoc.export.parquet.all('test.parquet')").consume()); + System.out.println("resultSummary = " + resultSummary); + + ResultSummary resultSummary1 = session.executeWrite(tx -> tx.run("CALL apoc.import.parquet('test.parquet')").consume()); + System.out.println("resultSummary1 = " + resultSummary1); + + ResultSummary resultSummary2 = session.executeWrite(tx -> tx.run("CALL apoc.load.parquet('test.parquet')").consume()); + System.out.println("resultSummary2 = " + resultSummary2); + + testResult(session, "CALL apoc.load.parquet('test.parquet')", r -> { + Map next = r.next(); + System.out.println("next = " + next); + + }); + + // todo - wait for next driver versions? + // --- session.run("MATCH (n) RETURN n").list() + /* + org.neo4j.driver.internal.util.ErrorUtil$InternalExceptionCause + detailMessage = "Struct tag: 0x56 representing type VECTOR is not supported for this protocol version" + cause = {Neo4jException@7483} "org.neo4j.driver.exceptions.Neo4jException: 22NBD: Unsupported struct tag: 0x56." + */ + + session.run("MATCH (n) RETURN n").list(); + + System.out.println("resultSummary2 = " + resultSummary2); + } + + @Test + public void testXls() { + // TODO + } + + @Test + public void testArrow() { + // TODO - import and export, since load that is in LoadArrowExtended + } + + @Test + public void testCsv() { + // TODO - test just the load, if the export is feasible, otherwise write a new issue or so + } +} diff --git a/extended/build.gradle b/extended/build.gradle index cc343f9b04..e364f3a677 100644 --- a/extended/build.gradle +++ b/extended/build.gradle @@ -102,7 +102,7 @@ dependencies { // They need to be provided either through the database or in an extra .jar compileOnly group: 'org.neo4j', name: 'neo4j', version: neo4jVersionEffective // same version as the one included in neo4j `lib` - compileOnly group: 'org.neo4j.driver', name: 'neo4j-java-driver', version: '5.28.7' + compileOnly group: 'org.neo4j.driver', name: 'neo4j-java-driver', version: '6.0.0-beta01' compileOnly group: 'org.apache.poi', name: 'poi', version: '5.1.0', { exclude group: 'org.apache.commons', module: 'commons-collections4' diff --git a/extended/src/main/java/apoc/load/xls/LoadXls.java b/extended/src/main/java/apoc/load/xls/LoadXls.java index 6d60cf5248..d7c8e140e8 100644 --- a/extended/src/main/java/apoc/load/xls/LoadXls.java +++ b/extended/src/main/java/apoc/load/xls/LoadXls.java @@ -212,6 +212,9 @@ private Object convertType(Object value) { return dateParse(value.toString(), OffsetTime.class, dateParse); case DURATION: return durationParse(value.toString()); + // TODO - vector + case VECTOR: + return null; default: return value; } diff --git a/extended/src/main/java/apoc/util/ExtendedUtil.java b/extended/src/main/java/apoc/util/ExtendedUtil.java index 0896bc4727..6403d6aeae 100644 --- a/extended/src/main/java/apoc/util/ExtendedUtil.java +++ b/extended/src/main/java/apoc/util/ExtendedUtil.java @@ -50,6 +50,8 @@ public static String dateFormat( TemporalAccessor value, String format){ public static double doubleValue( Entity pc, String prop, Number defaultValue) { return Util.toDouble(pc.getProperty(prop, defaultValue)); } + + // TODO - vector parse?? public static Duration durationParse(String value) { return Duration.parse(value); diff --git a/extended/src/test/java/apoc/ComparePerformancesTest.java b/extended/src/test/java/apoc/ComparePerformancesTest.java index 361123819b..24319a474e 100644 --- a/extended/src/test/java/apoc/ComparePerformancesTest.java +++ b/extended/src/test/java/apoc/ComparePerformancesTest.java @@ -26,6 +26,9 @@ @Ignore("This test compare import/export procedures performances, we ignore it since it's slow and just log the times spent") public class ComparePerformancesTest { + + // TODO - roundrip with these kind of procedures and vectors + private static final File directory = new File("target/import"); static { //noinspection ResultOfMethodCallIgnored directory.mkdirs(); diff --git a/extended/src/test/java/apoc/export/arrow/ImportArrowExtendedTest.java b/extended/src/test/java/apoc/export/arrow/ImportArrowExtendedTest.java index 504c53ffdd..33917adcce 100644 --- a/extended/src/test/java/apoc/export/arrow/ImportArrowExtendedTest.java +++ b/extended/src/test/java/apoc/export/arrow/ImportArrowExtendedTest.java @@ -3,6 +3,7 @@ import apoc.meta.Meta; import apoc.meta.MetaRestricted; import apoc.util.TestUtil; +import apoc.util.Util; import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -14,13 +15,17 @@ import java.io.File; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import static apoc.ApocConfig.APOC_EXPORT_FILE_ENABLED; import static apoc.ApocConfig.APOC_IMPORT_FILE_ENABLED; import static apoc.ApocConfig.apocConfig; import static apoc.export.arrow.ArrowTestUtil.ARROW_BASE_FOLDER; import static apoc.export.arrow.ArrowTestUtil.testImportCommon; +import static org.neo4j.configuration.SettingImpl.newBuilder; +import static org.neo4j.configuration.SettingValueParsers.BOOL; public class ImportArrowExtendedTest { private static File directory = new File(ARROW_BASE_FOLDER); @@ -29,13 +34,45 @@ public class ImportArrowExtendedTest { } private final Map MAPPING_ALL = Map.of("mapping", - Map.of("bffSince", "Duration", "place", "Point", "listInt", "LongArray", "born", "LocalDateTime") + Util.map("bffSince", "Duration", + "place", "Point", + "listInt", "LongArray", + "born", "LocalDateTime", + "INTEGER64", "vector", + "INTEGER32", "vector", + "INTEGER16", "vector", + "INTEGER8", "vector", + "FLOAT64", "vector", + "FLOAT32", "vector" + ) ); @ClassRule public static DbmsRule db = new ImpermanentDbmsRule() + + .withSetting( + GraphDatabaseSettings.procedure_unrestricted, + List.of( + "apoc.meta.nodes.count", + "apoc.meta.stats", + "apoc.meta.data", + "apoc.meta.schema", + "apoc.meta.nodeTypeProperties", + "apoc.meta.relTypeProperties", + "apoc.meta.graph", + "apoc.meta.graph.of", + "apoc.meta.graphSample", + "apoc.meta.subGraph")) + .withSetting(GraphDatabaseInternalSettings.cypher_enable_vector_type, true) + .withSetting( + newBuilder("internal.dbms.debug.track_cursor_close", BOOL, false) + .build(), + false) + .withSetting( + newBuilder("internal.dbms.debug.trace_cursors", BOOL, false).build(), false) + .withSetting(GraphDatabaseInternalSettings.cypher_enable_vector_type, true) .withSetting(GraphDatabaseInternalSettings.enable_experimental_cypher_versions, true) - .withSetting(GraphDatabaseSettings.load_csv_file_url_root, directory.toPath().toAbsolutePath()); + .withSetting(GraphDatabaseSettings.load_csv_file_url_root, directory.toPath().toAbsolutePath()); @@ -48,8 +85,17 @@ public static void beforeClass() { public void before() { db.executeTransactionally("MATCH (n) DETACH DELETE n"); + var vectorTypes1 = List.of("INT64", "INT32", "INT16", "INT8", "FLOAT64", "FLOAT32"); + for (String type : vectorTypes1) { + db.executeTransactionally("CYPHER 25 CREATE (:Foo { z: VECTOR([1, 2, 3], 3, %s) });".formatted(type)); + } + db.executeTransactionally("CREATE (f:User {name:'Adam',age:42,male:true,kids:['Sam','Anna','Grace'], born:localdatetime('2015-05-18T19:32:24.000'), place:point({latitude: 13.1, longitude: 33.46789, height: 100.0})})-[:KNOWS {since: 1993, bffSince: duration('P5M1.5D')}]->(b:User {name:'Jim',age:42})"); db.executeTransactionally("CREATE (:Another {foo:1, listInt: [1,2]}), (:Another {bar:'Sam'})"); +// var vectorTypes = List.of("INTEGER64", "INTEGER32", "INTEGER16", "INTEGER8", "FLOAT64","FLOAT32"); + var vectorTypes = List.of("INT64", "INT32", "INT16", "INT8", "FLOAT64", "FLOAT32"); + var types = vectorTypes.stream().map(i -> "%1$s: VECTOR([1,2,3], 3, %1$s)".formatted(i)).collect(Collectors.joining(",")); + db.executeTransactionally("CYPHER 25 CREATE (:Vectors {%s})".formatted(types)); apocConfig().setProperty(APOC_IMPORT_FILE_ENABLED, true); apocConfig().setProperty(APOC_EXPORT_FILE_ENABLED, true); diff --git a/extended/src/test/java/apoc/export/arrow/MetaTest.java b/extended/src/test/java/apoc/export/arrow/MetaTest.java new file mode 100644 index 0000000000..5e33c625cf --- /dev/null +++ b/extended/src/test/java/apoc/export/arrow/MetaTest.java @@ -0,0 +1,2452 @@ +/* + * 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.export.arrow; + +import apoc.graph.Graphs; +import apoc.meta.Meta; +import apoc.meta.MetaRestricted; +import apoc.meta.SampleMetaConfig; +import apoc.meta.Types; +import apoc.nodes.Nodes; +import apoc.util.MapUtil; +import apoc.util.TestUtil; +import apoc.util.Util; +import apoc.util.collection.Iterables; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.io.IOUtils; +import org.junit.*; +import org.mockito.Mockito; +import org.neo4j.configuration.GraphDatabaseInternalSettings; +import org.neo4j.configuration.GraphDatabaseSettings; +import org.neo4j.graphdb.*; +import org.neo4j.test.rule.DbmsRule; +import org.neo4j.test.rule.ImpermanentDbmsRule; +import org.neo4j.values.storable.*; + +import java.io.InputStreamReader; +import java.time.Clock; +import java.time.LocalDate; +import java.util.*; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static apoc.util.MapUtil.map; +import static apoc.util.TestUtil.testCall; +import static apoc.util.TestUtil.testResult; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.*; +import static org.neo4j.configuration.SettingImpl.newBuilder; +import static org.neo4j.configuration.SettingValueParsers.BOOL; +import static org.neo4j.driver.Values.isoDuration; +import static org.neo4j.graphdb.traversal.Evaluators.toDepth; + + +public class MetaTest { + + @Rule + public DbmsRule db = new ImpermanentDbmsRule() + .withSetting( + GraphDatabaseSettings.procedure_unrestricted, + List.of( + "apoc.meta.nodes.count", + "apoc.meta.stats", + "apoc.meta.data", + "apoc.meta.schema", + "apoc.meta.nodeTypeProperties", + "apoc.meta.relTypeProperties", + "apoc.meta.graph", + "apoc.meta.graph.of", + "apoc.meta.graphSample", + "apoc.meta.subGraph")) + .withSetting(GraphDatabaseInternalSettings.cypher_enable_vector_type, true) + .withSetting( + newBuilder("internal.dbms.debug.track_cursor_close", BOOL, false) + .build(), + false) + .withSetting( + newBuilder("internal.dbms.debug.trace_cursors", BOOL, false).build(), false); + + @Before + public void setUp() { + TestUtil.registerProcedure( + db, Meta.class, MetaRestricted.class, Graphs.class, Nodes.class); + } + + @After + public void teardown() { + db.shutdown(); + } + + public static boolean hasRecordMatching(List> records, Map record) { + return hasRecordMatching(records, row -> { + boolean okSoFar = true; + + for (String k : record.keySet()) { + okSoFar = okSoFar + && row.containsKey(k) + && (row.get(k) == null + ? (record.get(k) == null) + : row.get(k).equals(record.get(k))); + } + + return okSoFar; + }); + } + + public static boolean hasRecordMatching( + List> records, Predicate> predicate) { + return records.stream().anyMatch(predicate); + } + + public static List> gatherRecords(Result r) { + List> rows = new ArrayList<>(); + while (r.hasNext()) { + Map row = r.next(); + rows.add(row); + } + return rows; + } + // Can be valuable for debugging purposes + @SuppressWarnings("unused") + private static String toCSV(List> list) { + List headers = + list.stream().flatMap(map -> map.keySet().stream()).distinct().toList(); + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < headers.size(); i++) { + sb.append(headers.get(i)); + sb.append(i == headers.size() - 1 ? "\n" : ","); + } + for (Map map : list) { + for (int i = 0; i < headers.size(); i++) { + sb.append(map.get(headers.get(i))); + sb.append(i == headers.size() - 1 ? "\n" : ","); + } + } + return sb.toString(); + } + + public static boolean testDBCallEquivalence(GraphDatabaseService db, String testCall, String equivalentToCall) { + AtomicReference>> compareTo = new AtomicReference<>(); + AtomicReference>> testSet = new AtomicReference<>(); + + TestUtil.testResult(db, equivalentToCall, r -> compareTo.set(gatherRecords(r))); + + TestUtil.testResult(db, testCall, r -> testSet.set(gatherRecords(r))); + + // Uncomment this for debugging purposes + /* + System.out.println("COMPARE TO:"); + System.out.println(toCSV(compareTo.get())); + System.out.println("TEST SET:"); + System.out.println(toCSV(testSet.get())); + */ + + return resultSetsEquivalent(compareTo.get(), testSet.get()); + } + + public static boolean resultSetsEquivalent(List> baseSet, List> testSet) { + if (baseSet.size() != testSet.size()) { + System.err.println("Result sets have different cardinality"); + return false; + } + + boolean allMatch = true; + + for (Map baseRecord : baseSet) { + allMatch = allMatch && hasRecordMatching(testSet, baseRecord); + } + + return allMatch; + } + + @Test + public void testMetaGraphExtraRels() { + db.executeTransactionally( + """ + CREATE (a:S1 {SomeName1:'aaa'}) + CREATE (b:S2 {SomeName2:'bbb'}) + CREATE (c:S3 {SomeName3:'ccc'}) + CREATE (a)-[:HAS]->(b) + CREATE (b)-[:HAS]->(c)"""); + + testCall(db, "call apoc.meta.graph()", (row) -> { + List nodes = (List) row.get("nodes"); + List relationships = (List) row.get("relationships"); + assertEquals(3, nodes.size()); + assertEquals(2, relationships.size()); + }); + } + + @Test + public void testMetaGraphMaxRels() { + db.executeTransactionally("CREATE (:S2 {id:'another'}), (:S2 {id:'another2'}), (:S2 {id:'another3'}), \n" + + // create nodes to be linked + "(a:S1 {id:'aaa'}), (b:S2 {id:'bbb'}), (c:S3 {id:'ccc'}), (d:S4 {id:'ddd'}), " + + "(e:S5 {id:'eee'}), (f:S6 {id:'fff'}), (g:S7 {id:'ggg'})," + + + // create rels + "(a)-[:HAS]->(b), (a)-[:HAS]->(c), (a)-[:HAS]->(d), (a)-[:HAS]->(e), (a)-[:HAS]->(f), (a)-[:HAS]->(g)," + + "(b)-[:HAS]->(c), (b)-[:HAS]->(d), (b)-[:HAS]->(e), (b)-[:HAS]->(f), (b)-[:HAS]->(g)"); + + testCall(db, "call apoc.meta.graph()", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(11, relationships.size()); + }); + + testCall(db, "call apoc.meta.graph({maxRels: 1})", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(8, relationships.size()); + }); + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + + @Test + public void testMetaType() { + try (Transaction tx = db.beginTx()) { + Node node = tx.createNode(); + Relationship rel = node.createRelationshipTo(node, RelationshipType.withName("FOO")); + testTypeName(node, "NODE"); + testTypeName(rel, "RELATIONSHIP"); + Path path = tx.traversalDescription() + .evaluator(toDepth(1)) + .traverse(node) + .iterator() + .next(); + // TODO PATH FAILS testTypeName(path, "PATH"); + tx.rollback(); + } + testTypeName(singletonMap("a", 10), "MAP"); + testTypeName(asList(1, 2), "LIST OF INTEGER"); + testTypeName(1L, "INTEGER"); + testTypeName(1, "INTEGER"); + testTypeName(1.0D, "FLOAT"); + testTypeName(1.0, "FLOAT"); + testTypeName("a", "STRING"); + testTypeName(false, "BOOLEAN"); + testTypeName(true, "BOOLEAN"); + testTypeName(null, "NULL"); + } + + @Test + public void testMetaTypeArray() { + testTypeName(asList(1, 2), "LIST OF INTEGER"); + testTypeName(asList(LocalDate.of(2018, 1, 1), 2), "LIST OF ANY"); + testTypeName(new Integer[] {1, 2}, "LIST OF INTEGER"); + testTypeName(new Float[] {1f, 2f}, "LIST OF FLOAT"); + testTypeName(new Double[] {1d, 2d}, "LIST OF FLOAT"); + testTypeName(new String[] {"a", "b"}, "LIST OF STRING"); + testTypeName(new Long[] {1L, 2L}, "LIST OF INTEGER"); + testTypeName(new LocalDate[] {LocalDate.of(2018, 1, 1), LocalDate.of(2018, 1, 1)}, "LIST OF DATE"); + testTypeName(new Object[] {1d, ""}, "LIST OF ANY"); + } + + @Test + public void testMetaIsType() { + try (Transaction tx = db.beginTx()) { + Node node = tx.createNode(); + Relationship rel = node.createRelationshipTo(node, RelationshipType.withName("FOO")); + testIsTypeName(node, "NODE"); + testIsTypeName(rel, "RELATIONSHIP"); + Path path = tx.traversalDescription() + .evaluator(toDepth(1)) + .traverse(node) + .iterator() + .next(); + // TODO PATH FAILS testIsTypeName(path, "PATH"); + tx.rollback(); + } + testIsTypeName(singletonMap("a", 10), "MAP"); + testIsTypeName(asList(1, 2), "LIST OF INTEGER"); + testIsTypeName(1L, "INTEGER"); + testIsTypeName(1, "INTEGER"); + testIsTypeName(1.0D, "FLOAT"); + testIsTypeName(1.0, "FLOAT"); + testIsTypeName("a", "STRING"); + testIsTypeName(false, "BOOLEAN"); + testIsTypeName(true, "BOOLEAN"); + testIsTypeName(null, "NULL"); + } + + @Test + public void testMetaTypes() { + + Map param = map( + "MAP", + singletonMap("a", 10), + "LIST OF INTEGER", + asList(1, 2), + "INTEGER", + 1L, + "FLOAT", + 1.0D, + "STRING", + "a", + "BOOLEAN", + true, + "NULL", + null); + TestUtil.testCall(db, "RETURN apoc.meta.cypher.types($param) AS value", singletonMap("param", param), row -> { + Map res = (Map) row.get("value"); + res.forEach(Assert::assertEquals); + }); + } + + private void testTypeName(Object value, String type) { + TestUtil.testCall( + db, + "RETURN apoc.meta.cypher.type($value) AS value", + singletonMap("value", value), + row -> assertEquals(type, row.get("value"))); + } + + @Test + public void testVectorTypes() { + var vectorTypes = List.of("INT64", "INT32", "INT16", "INT8", "FLOAT64", "FLOAT32"); + for (String type : vectorTypes) { + TestUtil.testCall( + db, + """ + CYPHER 25 + WITH VECTOR([1, 2, 3], 3, %s) AS v + RETURN apoc.meta.cypher.type(v) AS value""" + .formatted(type), + row -> assertEquals("VECTOR", row.get("value"))); + + TestUtil.testCall( + db, + """ + CYPHER 25 + WITH VECTOR([1, 2, 3], 3, %s) AS v + RETURN apoc.meta.cypher.isType(v, "VECTOR") AS value""" + .formatted(type), + row -> assertEquals(true, row.get("value"))); + } + } + + private void testIsTypeName(Object value, String type) { + TestUtil.testCall( + db, + "RETURN apoc.meta.cypher.isType($value,$type) AS value", + map("value", value, "type", type), + result -> assertEquals("type was not " + type, true, result.get("value"))); + TestUtil.testCall( + db, + "RETURN apoc.meta.cypher.isType($value,$type) AS value", + map("value", value, "type", type + "foo"), + result -> assertEquals(false, result.get("value"))); + } + + private void assertStats(String setupQuery, Map expected) { + // Stats works on committed data + db.executeTransactionally(setupQuery); + try (final var tx = db.beginTx()) { + assertThat(tx.execute("CALL apoc.meta.stats()").stream().toList()) + .satisfiesExactly(row -> assertThat(row).containsExactlyInAnyOrderEntriesOf(expected)); + tx.commit(); + } + + // Stats works on uncommited data + db.executeTransactionally("CREATE (:UnrelatedLabel)-[:UNRELATED_REL]->()"); + try (final var tx = db.beginTx()) { + assertThat(tx.execute("MATCH (n) DETACH DELETE n").stream().toList()) + .size() + .isLessThanOrEqualTo(0); + assertThat(tx.execute(setupQuery).stream().toList()).size().isGreaterThanOrEqualTo(0); + assertThat(tx.execute("CALL apoc.meta.stats()").stream().toList()) + .satisfiesExactly(row -> assertThat(row).containsExactlyInAnyOrderEntriesOf(expected)); + tx.commit(); + } + } + + private Map statsMap(Map values) { + final var stats = values.entrySet().stream() + .filter(e -> !"relTypesCount".equals(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return ImmutableMap.builder() + .putAll(values) + .put("stats", stats) + .build(); + } + + @Test + public void testMetaStats() { + final var setup = + "CREATE (:Actor)-[:ACTED_IN]->(:Movie), ()-[:ACTED_IN]->(:Movie), (:Actor)-[:ACTED_IN]->(), ()-[:ACTED_IN]->()"; + final var expected = statsMap(Map.of( + "relTypeCount", 1L, + "propertyKeyCount", 0L, + "labelCount", 2L, + "nodeCount", 8L, + "relCount", 4L, + "labels", Map.of("Movie", 2L, "Actor", 2L), + "relTypes", + Map.of( + "()-[:ACTED_IN]->(:Movie)", + 2L, + "()-[:ACTED_IN]->()", + 4L, + "(:Actor)-[:ACTED_IN]->()", + 2L), + "relTypesCount", Map.of("ACTED_IN", 4L))); + assertStats(setup, expected); + } + + @Test + public void testMetaStats2() { + final var nodeLabels = List.of("", ":A", ":B", ":A:B"); + final var setup = new StringBuilder(); + for (final var labelsA : nodeLabels) { + setup.append("CREATE (%s)%n".formatted(labelsA)); + for (final var labelsB : nodeLabels) { + setup.append("CREATE (%s)-[:R1]->(%s)%n".formatted(labelsA, labelsB)); + setup.append("CREATE (%s)<-[:R2]-(%s)%n".formatted(labelsA, labelsB)); + } + } + final var expected = statsMap(Map.of( + "relTypeCount", 2L, + "propertyKeyCount", 0L, + "labelCount", 2L, + "nodeCount", 68L, + "relCount", 32L, + "labels", Map.of("A", 34L, "B", 34L), + "relTypes", + Map.of( + "()-[:R1]->()", 16L, + "()-[:R2]->()", 16L, + "()-[:R1]->(:A)", 8L, + "()-[:R1]->(:B)", 8L, + "()-[:R2]->(:A)", 8L, + "()-[:R2]->(:B)", 8L, + "(:A)-[:R1]->()", 8L, + "(:A)-[:R2]->()", 8L, + "(:B)-[:R1]->()", 8L, + "(:B)-[:R2]->()", 8L), + "relTypesCount", Map.of("R1", 16L, "R2", 16L))); + assertStats(setup.toString(), expected); + } + + @Test + public void testMetaGraph() { + db.executeTransactionally("CREATE (a:Actor)-[:ACTED_IN]->(m1:Movie),(a)-[:ACTED_IN]->(m2:Movie)"); + TestUtil.testCall(db, "CALL apoc.meta.graph()", (row) -> { + List nodes = (List) row.get("nodes"); + Node n1 = nodes.get(0); + assertTrue(n1.hasLabel(Label.label("Actor"))); + assertEquals(1L, n1.getProperty("count")); + assertEquals("Actor", n1.getProperty("name")); + Node n2 = nodes.get(1); + assertTrue(n2.hasLabel(Label.label("Movie"))); + assertEquals("Movie", n2.getProperty("name")); + assertEquals(2L, n2.getProperty("count")); + List rels = (List) row.get("relationships"); + Relationship rel = rels.iterator().next(); + assertEquals("ACTED_IN", rel.getType().name()); + assertEquals(2L, rel.getProperty("count")); + }); + } + + @Test + public void testMetaGraph2() { + db.executeTransactionally("CREATE (:Actor)-[:ACTED_IN]->(:Movie) "); + TestUtil.testCall(db, "CALL apoc.meta.graphSample()", (row) -> { + List nodes = (List) row.get("nodes"); + Node n1 = nodes.get(0); + assertTrue(n1.hasLabel(Label.label("Actor"))); + assertEquals(1L, n1.getProperty("count")); + assertEquals("Actor", n1.getProperty("name")); + Node n2 = nodes.get(1); + assertTrue(n2.hasLabel(Label.label("Movie"))); + assertEquals("Movie", n2.getProperty("name")); + assertEquals(1L, n1.getProperty("count")); + List rels = (List) row.get("relationships"); + Relationship rel = rels.iterator().next(); + assertEquals("ACTED_IN", rel.getType().name()); + assertEquals(1L, rel.getProperty("count")); + }); + } + + @Test + public void testMetaData() { + db.executeTransactionally("create index for (n:Movie) on (n.title)"); + db.executeTransactionally("create constraint for (a:Actor) require a.name is unique"); + db.executeTransactionally( + """ + CREATE (actor1:Actor {name:'Tom Hanks'})-[:ACTED_IN {roles:'Forrest'}]->(movie1:Movie {title:'Forrest Gump'}), + (actor2:Actor {name: 'Bruce Lee'})-[:ACTED_IN {roles:'FooBaz'}]->(movie1), + (actor1)-[:ACTED_IN {roles:'Movie2Role'}]->(movie2:Movie {title:'Movie2'}), (actor1)-[:ACTED_IN {roles:'Movie3Role'}]->(movie3:Movie {title:'Movie3'}), + (actor1)-[:DIRECTED {foo: 'first'}]->(movie2), (actor1)-[:DIRECTED {foo: 'second'}]->(:Movie {title:'Movie4'}), + (:Studio {name: 'Pixar'})-[:ANIMATED {bar: 'alpha'}]->(movie2)"""); + TestUtil.testResult( + db, + """ + CALL apoc.meta.data() + YIELD label, property, count, unique, index, existence, type, array, left, right, other, otherLabels, elementType + RETURN * ORDER BY elementType, property""", + (r) -> { + Map row = r.next(); + assertEquals("node", row.get("elementType")); + assertEquals("ACTED_IN", row.get("property")); + assertEquals("Actor", row.get("label")); + assertRelationshipActedInMetaData(row); + row = r.next(); + assertEquals("node", row.get("elementType")); + assertEquals("ANIMATED", row.get("property")); + assertEquals("Studio", row.get("label")); + assertRelationshipsAnimatedMetaData(row); + row = r.next(); + assertEquals("node", row.get("elementType")); + assertEquals("DIRECTED", row.get("property")); + assertEquals("Actor", row.get("label")); + assertRelationshipsDirectedMetaData(row); + row = r.next(); + assertEquals("node", row.get("elementType")); + assertPropertiesMetaData(row); + row = r.next(); + assertEquals("node", row.get("elementType")); + assertPropertiesMetaData(row); + row = r.next(); + assertEquals("node", row.get("elementType")); + assertPropertiesMetaData(row); + row = r.next(); + assertEquals("relationship", row.get("elementType")); + assertEquals("ACTED_IN", row.get("label")); + assertEquals("Actor", row.get("property")); + assertRelationshipActedInMetaData(row); + row = r.next(); + assertEquals("relationship", row.get("elementType")); + assertEquals("DIRECTED", row.get("label")); + assertEquals("Actor", row.get("property")); + assertRelationshipsDirectedMetaData(row); + row = r.next(); + assertEquals("relationship", row.get("elementType")); + assertEquals("ANIMATED", row.get("label")); + assertEquals("Studio", row.get("property")); + assertRelationshipsAnimatedMetaData(row); + row = r.next(); + assertEquals("relationship", row.get("elementType")); + assertPropertiesMetaData(row); + row = r.next(); + assertEquals("relationship", row.get("elementType")); + assertPropertiesMetaData(row); + row = r.next(); + assertEquals("relationship", row.get("elementType")); + assertPropertiesMetaData(row); + assertFalse(r.hasNext()); + }); + } + + private void assertRelationshipsDirectedMetaData(Map row) { + assertRowMetaData(row, 1L, 2L, 0L, Types.RELATIONSHIP); + } + + private void assertRelationshipsAnimatedMetaData(Map row) { + assertRowMetaData(row, 1L, 1L, 0L, Types.RELATIONSHIP); + } + + private void assertRelationshipActedInMetaData(Map row) { + assertRowMetaData(row, 2L, 2L, 0L, Types.RELATIONSHIP); + } + + private void assertPropertiesMetaData(Map row) { + assertRowMetaData(row, 0L, 0L, 0L, Types.STRING); + } + + private void assertRowMetaData(Map row, long count, long left, long right, Types type) { + assertEquals(count, row.get("count")); + assertEquals(left, row.get("left")); + assertEquals(right, row.get("right")); + assertEquals(type.name(), row.get("type")); + } + + @Test + public void testMetaSchema() { + db.executeTransactionally("create index for (n:Movie) on (n.title)"); + db.executeTransactionally("create constraint for (p:Person) require p.name is unique"); + db.executeTransactionally( + "CREATE (:Person:Actor:Director {name:'Tom', born:'05-06-1956', dead:false})-[:ACTED_IN {roles:'Forrest'}]->(:Movie {title:'Forrest Gump'})"); + testCall(db, "CALL apoc.meta.schema()", (row) -> { + List emprtyList = new ArrayList<>(); + List fullList = Arrays.asList("Actor", "Director"); + + Map o = (Map) row.get("value"); + assertEquals(5, o.size()); + + Map movie = (Map) o.get("Movie"); + Map movieProperties = (Map) movie.get("properties"); + Map movieTitleProperties = (Map) movieProperties.get("title"); + assertNotNull(movie); + assertEquals("node", movie.get("type")); + assertEquals(1L, movie.get("count")); + assertEquals(emprtyList, movie.get("labels")); + assertEquals(4, movieTitleProperties.size()); + assertEquals("STRING", movieTitleProperties.get("type")); + assertEquals(true, movieTitleProperties.get("indexed")); + assertEquals(false, movieTitleProperties.get("unique")); + Map movieRel = (Map) movie.get("relationships"); + Map movieActedIn = (Map) movieRel.get("ACTED_IN"); + assertEquals(1L, movieRel.size()); + assertEquals("in", movieActedIn.get("direction")); + assertEquals(1L, movieActedIn.get("count")); + assertEquals(Arrays.asList("Person", "Actor", "Director"), movieActedIn.get("labels")); + + Map person = (Map) o.get("Person"); + Map personProperties = (Map) person.get("properties"); + Map personNameProperty = (Map) personProperties.get("name"); + assertNotNull(person); + assertEquals("node", person.get("type")); + assertEquals(1L, person.get("count")); + assertEquals(fullList, person.get("labels")); + assertEquals(true, personNameProperty.get("unique")); + assertEquals(3, personProperties.size()); + + Map actor = (Map) o.get("Actor"); + assertNotNull(actor); + assertEquals("node", actor.get("type")); + assertEquals(1L, actor.get("count")); + assertEquals(emprtyList, actor.get("labels")); + + Map director = (Map) o.get("Director"); + Map directorProperties = (Map) director.get("properties"); + assertNotNull(director); + assertEquals("node", director.get("type")); + assertEquals(1L, director.get("count")); + assertEquals(emprtyList, director.get("labels")); + assertEquals(3, directorProperties.size()); + + Map actedIn = (Map) o.get("ACTED_IN"); + Map actedInProperties = (Map) actedIn.get("properties"); + Map actedInRoleProperty = (Map) actedInProperties.get("roles"); + assertNotNull(actedIn); + assertEquals("relationship", actedIn.get("type")); + assertEquals("STRING", actedInRoleProperty.get("type")); + assertEquals(false, actedInRoleProperty.get("array")); + assertEquals(false, actedInRoleProperty.get("existence")); + }); + } + + @Test + public void testMetaSchemaWithNodesAndRelsWithoutProps() { + db.executeTransactionally( + "CREATE (:Other), (:Other)-[:REL_1]->(:Movie)<-[:REL_2 {baz: 'baa'}]-(:Director), (:Director {alpha: 'beta'}), (:Actor {foo:'bar'}), (:Person)"); + testCall(db, "CALL apoc.meta.schema()", (row) -> { + Map value = (Map) row.get("value"); + assertEquals(7, value.size()); + + Map other = (Map) value.get("Other"); + Map otherProperties = (Map) other.get("properties"); + assertEquals(0, otherProperties.size()); + assertEquals("node", other.get("type")); + assertEquals(2L, other.get("count")); + Map Movie = (Map) value.get("Movie"); + Map movieProperties = (Map) Movie.get("properties"); + assertEquals(0, movieProperties.size()); + assertEquals("node", Movie.get("type")); + assertEquals(1L, Movie.get("count")); + Map director = (Map) value.get("Director"); + Map directorProperties = (Map) director.get("properties"); + assertEquals(1, directorProperties.size()); + assertEquals("node", director.get("type")); + assertEquals(2L, director.get("count")); + Map person = (Map) value.get("Person"); + Map personProperties = (Map) person.get("properties"); + assertEquals(0, personProperties.size()); + assertEquals("node", person.get("type")); + assertEquals(1L, person.get("count")); + Map actor = (Map) value.get("Actor"); + Map actorProperties = (Map) actor.get("properties"); + assertEquals(1, actorProperties.size()); + assertEquals("node", actor.get("type")); + assertEquals(1L, actor.get("count")); + + Map rel1 = (Map) value.get("REL_1"); + Map rel1Properties = (Map) rel1.get("properties"); + assertEquals(0, rel1Properties.size()); + assertEquals("relationship", rel1.get("type")); + assertEquals(1L, rel1.get("count")); + Map rel2 = (Map) value.get("REL_2"); + Map rel2Properties = (Map) rel2.get("properties"); + assertEquals(1, rel2Properties.size()); + assertEquals("relationship", rel2.get("type")); + assertEquals(1L, rel2.get("count")); + }); + } + + @Test + public void testMetaSchemaWithSmallSampleAndRelationships() { + final List labels = List.of("Other", "Foo"); + db.executeTransactionally( + "CREATE (:Foo), (:Other)-[:REL_0]->(:Other), (:Other)-[:REL_1]->(:Other)<-[:REL_2 {baz: 'baa'}]-(:Other), (:Other {alpha: 'beta'}), (:Other {foo:'bar'})-[:REL_3]->(:Other)"); + testCall( + db, "CALL apoc.meta.schema({sample: 2})", (row) -> ((Map>) row.get("value")) + .forEach((key, value) -> { + if (labels.contains(key)) { + assertEquals("node", value.get("type")); + } else { + assertEquals("relationship", value.get("type")); + } + })); + } + + @Test + public void testIssue1861LabelAndTypeWithSameName() { + db.executeTransactionally( + """ + CREATE (s0 :person{id:1} ) SET s0.name = 'rose' + CREATE (t0 :person{id:2}) SET t0.name = 'jack' + MERGE (s0) -[r0:person {alfa: 'beta'}] -> (t0)"""); + testCall(db, "CALL apoc.meta.schema()", (row) -> { + Map value = (Map) row.get("value"); + assertEquals(2, value.size()); + + Map personRelationship = (Map) value.get("person (RELATIONSHIP)"); + assertEquals(1L, personRelationship.get("count")); + assertEquals("relationship", personRelationship.get("type")); + Map relationshipProps = (Map) personRelationship.get("properties"); + assertEquals(Set.of("alfa"), relationshipProps.keySet()); + + Map personNode = (Map) value.get("person"); + assertEquals(2L, personNode.get("count")); + assertEquals("node", personNode.get("type")); + Map nodeProps = (Map) personNode.get("properties"); + assertEquals(Set.of("name", "id"), nodeProps.keySet()); + }); + } + + @Test + public void testSubGraphNoLimits() { + db.executeTransactionally("CREATE (:A)-[:X]->(b:B),(b)-[:Y]->(:C)"); + testCall(db, "CALL apoc.meta.subGraph({})", (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(3, nodes.size()); + assertTrue(nodes.stream() + .map(n -> Iterables.first(n.getLabels()).name()) + .allMatch(n -> n.equals("A") || n.equals("B") || n.equals("C"))); + assertEquals(2, rels.size()); + assertTrue(rels.stream().map(r -> r.getType().name()).allMatch(n -> n.equals("X") || n.equals("Y"))); + }); + } + + @Test + public void testSubGraphNonExistingLabels() { + db.executeTransactionally( + """ + MATCH (n) DETACH DELETE n + WITH COUNT(1) as foo + CREATE (:Foo), (:Bar), (:Test) // label Test created here + WITH COUNT(1) as foo + MATCH (t:Test) DELETE t // node with label Test delete again + WITH COUNT(1) as foo + MATCH (n) + RETURN n + """); + // Check for a completely new label + testCall(db, "CALL apoc.meta.subGraph({includeLabels:['X'],rels:[],excludes:[]})", (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(0, nodes.size()); + assertEquals(0, rels.size()); + }); + // Check for a previous existing label + testCall(db, "CALL apoc.meta.subGraph({includeLabels:['Test'],rels:[],excludes:[]})", (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(0, nodes.size()); + assertEquals(0, rels.size()); + }); + } + + @Test + public void testSubGraphNonExistingTypes() { + db.executeTransactionally( + """ + MATCH (n) DETACH DELETE n + WITH COUNT(1) as foo + CREATE (:Foo)-[:R]->(:Bar)-[:Rtest]->(:Test) + WITH COUNT(1) as foo + MATCH (t:Test) DETACH DELETE t + WITH COUNT(1) as foo + MATCH (n) + RETURN n + """); + // Check for a completely new type + testCall(db, "CALL apoc.meta.subGraph({includeLabels:[],includeRels:['X'],excludes:[]})", (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(2, nodes.size()); + assertEquals(0, rels.size()); + }); + // Check for a previous existing type + testCall(db, "CALL apoc.meta.subGraph({includeLabels:[],includeRels:['Rtest'],excludes:[]})", (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(2, nodes.size()); + assertEquals(0, rels.size()); + }); + } + + @Test + public void testSubGraphLimitLabels() { + final String labels = "labels"; + testSubgraphLabelsCommon(labels); + } + + private void testSubgraphLabelsCommon(String labels) { + db.executeTransactionally("CREATE (:A)-[:X]->(b:B),(b)-[:Y]->(:C)"); + testCall(db, "CALL apoc.meta.subGraph($conf)", map("conf", map(labels, List.of("A", "B"))), (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(2, nodes.size()); + assertTrue(nodes.stream() + .map(n -> Iterables.first(n.getLabels()).name()) + .allMatch(n -> n.equals("A") || n.equals("B"))); + assertEquals(1, rels.size()); + assertTrue(rels.stream().map(r -> r.getType().name()).allMatch(n -> n.equals("X"))); + }); + } + + @Test + public void testSubGraphWithIncludeLabels() { + final String labels = "includeLabels"; + testSubgraphLabelsCommon(labels); + } + + @Test + public void testSubGraphLimitWithRels() { + final String relsConf = "rels"; + assertMetaSubgraphCommon(relsConf); + } + + @Test + public void testSubGraphLimitWithIncludeRels() { + final String relsConf = "includeRels"; + assertMetaSubgraphCommon(relsConf); + } + + private void assertMetaSubgraphCommon(String relsConf) { + final Consumer> consumer = (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(3, nodes.size()); + assertTrue(nodes.stream() + .map(n -> Iterables.first(n.getLabels()).name()) + .allMatch(n -> n.equals("A") || n.equals("B") || n.equals("C"))); + assertEquals(1, rels.size()); + assertTrue(rels.stream().map(r -> r.getType().name()).allMatch(n -> n.equals("X"))); + }; + final Map conf = map(relsConf, List.of("X")); + testGraphCommon(conf, consumer); + } + + @Test + public void testSubGraphExcludes() { + final String relsConf = "excludes"; + testExcludeLabelsCommon(relsConf); + } + + @Test + public void testSubGraphExcludesLabels() { + final String relsConf = "excludeLabels"; + testExcludeLabelsCommon(relsConf); + } + + private void testGraphCommon(Map conf, Consumer> consumer) { + db.executeTransactionally("CREATE (:A)-[:X]->(b:B),(b)-[:Y]->(:C)"); + testCall(db, "CALL apoc.meta.subGraph($conf)", map("conf", conf), consumer); + } + + private void testExcludeLabelsCommon(String relsConf) { + final Consumer> consumer = (row) -> { + List nodes = (List) row.get("nodes"); + List rels = (List) row.get("relationships"); + assertEquals(2, nodes.size()); + assertTrue(nodes.stream() + .map(n -> Iterables.first(n.getLabels()).name()) + .allMatch(n -> n.equals("A") || n.equals("C"))); + assertEquals(0, rels.size()); + }; + final Map conf = map(relsConf, List.of("B")); + testGraphCommon(conf, consumer); + } + + @Test + public void testMetaSubgraphBothIncludeAndExclude() { + final Consumer> consumer = (row) -> { + assertEquals(Collections.emptyList(), row.get("nodes")); + assertEquals(Collections.emptyList(), row.get("relationships")); + }; + final Map conf = map("excludeLabels", List.of("B"), "includeLabels", List.of("B")); + testGraphCommon(conf, consumer); + } + + @Test + public void testMetaDate() { + + Map param = map( + "DATE", DateValue.now(Clock.systemDefaultZone()), + "LOCAL_DATE", LocalDateTimeValue.now(Clock.systemDefaultZone()), + "TIME", TimeValue.now(Clock.systemDefaultZone()), + "LOCAL_TIME", LocalTimeValue.now(Clock.systemDefaultZone()), + "DATE_TIME", DateTimeValue.now(Clock.systemDefaultZone()), + "NULL", null); + + TestUtil.testCall(db, "RETURN apoc.meta.cypher.types($param) AS value", singletonMap("param", param), row -> { + Map r = (Map) row.get("value"); + + assertEquals("DATE", r.get("DATE")); + assertEquals("LOCAL_DATE_TIME", r.get("LOCAL_DATE")); + assertEquals("TIME", r.get("TIME")); + assertEquals("LOCAL_TIME", r.get("LOCAL_TIME")); + assertEquals("DATE_TIME", r.get("DATE_TIME")); + assertEquals("NULL", r.get("NULL")); + }); + } + + @Test + public void testMetaArray() { + + Map param = map( + "ARRAY", new String[] {"a", "b", "c"}, + "ARRAY_FLOAT", new Float[] {1.2f, 2.2f}, + "ARRAY_DOUBLE", new Double[] {1.2, 2.2}, + "ARRAY_INT", new Integer[] {1, 2}, + "ARRAY_OBJECT", new Object[] {1, "a"}, + "ARRAY_POINT", + new Object[] { + Values.pointValue(CoordinateReferenceSystem.WGS_84, 56.d, 12.78), + Values.pointValue(CoordinateReferenceSystem.WGS_84_3D, 56.d, 12.78, 100) + }, + "ARRAY_DURATION", + new Object[] { + isoDuration(5, 1, 43200, 0).asIsoDuration(), + isoDuration(2, 1, 125454, 0).asIsoDuration() + }, + "ARRAY_ARRAY", + new Object[] { + 1, + "a", + new Object[] {"a", 1}, + isoDuration(5, 1, 43200, 0).asIsoDuration() + }, + "NULL", null); + + TestUtil.testCall(db, "RETURN apoc.meta.cypher.types($param) AS value", singletonMap("param", param), row -> { + Map r = (Map) row.get("value"); + + assertEquals("LIST OF STRING", r.get("ARRAY")); + assertEquals("LIST OF FLOAT", r.get("ARRAY_FLOAT")); + assertEquals("LIST OF FLOAT", r.get("ARRAY_DOUBLE")); + assertEquals("LIST OF INTEGER", r.get("ARRAY_INT")); + assertEquals("LIST OF ANY", r.get("ARRAY_OBJECT")); + assertEquals("LIST OF POINT", r.get("ARRAY_POINT")); + assertEquals("LIST OF DURATION", r.get("ARRAY_DURATION")); + assertEquals("LIST OF ANY", r.get("ARRAY_ARRAY")); + assertEquals("NULL", r.get("NULL")); + }); + } + + @Test + public void testMetaNumber() { + + Map param = map("INTEGER", 1L, "FLOAT", 1.0f, "DOUBLE", 1.0D, "NULL", null); + + TestUtil.testCall(db, "RETURN apoc.meta.cypher.types($param) AS value", singletonMap("param", param), row -> { + Map r = (Map) row.get("value"); + + assertEquals("INTEGER", r.get("INTEGER")); + assertEquals("FLOAT", r.get("FLOAT")); + assertEquals("FLOAT", r.get("DOUBLE")); + assertEquals("NULL", r.get("NULL")); + }); + } + + @Test + public void testMeta() { + + Map param = map( + "LIST", asList(1.2, 2.1), + "STRING", "a", + "BOOLEAN", true, + "CHAR", 'a', + "DURATION", 'a', + "POINT_2D", Values.pointValue(CoordinateReferenceSystem.WGS_84, 56.d, 12.78), + "POINT_3D", Values.pointValue(CoordinateReferenceSystem.WGS_84_3D, 56.7, 12.78, 100.0), + "POINT_XYZ_2D", Values.pointValue(CoordinateReferenceSystem.CARTESIAN, 2.3, 4.5), + "POINT_XYZ_3D", Values.pointValue(CoordinateReferenceSystem.CARTESIAN_3D, 2.3, 4.5, 1.2), + "DURATION", isoDuration(5, 1, 43200, 0).asIsoDuration(), + "MAP", Util.map("a", "b"), + "NULL", null); + + TestUtil.testCall(db, "RETURN apoc.meta.cypher.types($param) AS value", singletonMap("param", param), row -> { + Map r = (Map) row.get("value"); + + assertEquals("LIST OF FLOAT", r.get("LIST")); + assertEquals("STRING", r.get("STRING")); + assertEquals("BOOLEAN", r.get("BOOLEAN")); + assertEquals("Character", r.get("CHAR")); + assertEquals("POINT", r.get("POINT_2D")); + assertEquals("POINT", r.get("POINT_3D")); + assertEquals("POINT", r.get("POINT_XYZ_2D")); + assertEquals("POINT", r.get("POINT_XYZ_3D")); + assertEquals("DURATION", r.get("DURATION")); + assertEquals("MAP", r.get("MAP")); + assertEquals("NULL", r.get("NULL")); + }); + } + + @Test + public void testMetaList() { + + Map param = map( + "LIST FLOAT", asList(1.2F, 2.1F), + "LIST STRING", asList("a", "b"), + "LIST CHAR", asList('a', 'a'), + "LIST DATE", asList(LocalDate.of(2018, 1, 1), LocalDate.of(2018, 2, 2)), + "LIST ANY", asList("test", 1, "asd", isoDuration(5, 1, 43200, 0).asIsoDuration()), + "LIST NULL", asList("test", null), + "LIST POINT", + asList( + Values.pointValue(CoordinateReferenceSystem.WGS_84, 56.d, 12.78), + Values.pointValue(CoordinateReferenceSystem.CARTESIAN_3D, 2.3, 4.5, 1.2)), + "LIST DURATION", + asList( + isoDuration(5, 1, 43200, 0).asIsoDuration(), + isoDuration(2, 1, 125454, 0).asIsoDuration()), + "LIST OBJECT", new Object[] {LocalDate.of(2018, 1, 1), "test"}, + "LIST OF LIST", asList(asList("a", "b", "c"), asList("aa", "bb", "cc"), asList("aaa", "bbb", "ccc")), + "LIST DOUBLE", asList(1.2D, 2.1D)); + + TestUtil.testCall(db, "RETURN apoc.meta.cypher.types($param) AS value", singletonMap("param", param), row -> { + Map r = (Map) row.get("value"); + + assertEquals("LIST OF FLOAT", r.get("LIST FLOAT")); + assertEquals("LIST OF STRING", r.get("LIST STRING")); + assertEquals("LIST OF ANY", r.get("LIST CHAR")); + assertEquals("LIST OF DATE", r.get("LIST DATE")); + assertEquals("LIST OF FLOAT", r.get("LIST DOUBLE")); + assertEquals("LIST OF POINT", r.get("LIST POINT")); + assertEquals("LIST OF DURATION", r.get("LIST DURATION")); + assertEquals("LIST OF ANY", r.get("LIST ANY")); + assertEquals("LIST OF ANY", r.get("LIST OBJECT")); + assertEquals("LIST OF LIST", r.get("LIST OF LIST")); + assertEquals("LIST OF ANY", r.get("LIST NULL")); + }); + } + + @Test + public void testMetaPoint() { + db.executeTransactionally("CREATE (:TEST {born:point({ longitude: 56.7, latitude: 12.78, height: 100 })})"); + + TestUtil.testCall( + db, + "MATCH (t:TEST) WITH t.born as born RETURN apoc.meta.cypher.type(born) AS value", + row -> assertEquals("POINT", row.get("value"))); + } + + @Test + public void testMetaDuration() { + db.executeTransactionally("CREATE (:TEST {duration:duration('P5M1DT12H')})"); + + TestUtil.testCall( + db, + "MATCH (t:TEST) WITH t.duration as duration RETURN apoc.meta.cypher.type(duration) AS value", + row -> assertEquals("DURATION", row.get("value"))); + } + + @Test + public void testMetaDataWithSample() { + db.executeTransactionally("create index for (n:Person) on (n.name)"); + db.executeTransactionally("CREATE (:Person {name:'Tom'})"); + db.executeTransactionally("CREATE (:Person {name:'John', surname:'Brown'})"); + db.executeTransactionally("CREATE (:Person {name:'Nick'})"); + db.executeTransactionally("CREATE (:Person {name:'Daisy', surname:'Bob'})"); + db.executeTransactionally("CREATE (:Person {name:'Elizabeth'})"); + db.executeTransactionally("CREATE (:Person {name:'Jack', surname:'White'})"); + db.executeTransactionally("CREATE (:Person {name:'Joy'})"); + db.executeTransactionally("CREATE (:Person {name:'Sarah', surname:'Taylor'})"); + db.executeTransactionally("CREATE (:Person {name:'Jane'})"); + db.executeTransactionally("CREATE (:Person {name:'Jeff', surname:'Logan'})"); + TestUtil.testResult(db, "CALL apoc.meta.data({sample:2})", (r) -> assertThat( + r.stream().map(m -> m.get("property"))) + .containsExactlyInAnyOrder("name", "surname")); + } + + @Test + public void testMetaDataWithSampleNormalized() { + db.executeTransactionally("create index for (n:Person) on (n.name)"); + db.executeTransactionally("CREATE (:Person {name:'Tom'})"); + db.executeTransactionally("CREATE (:Person {name:'John'})"); + db.executeTransactionally("CREATE (:Person {name:'Nick'})"); + db.executeTransactionally("CREATE (:Person {name:'Daisy', surname:'Bob'})"); + db.executeTransactionally("CREATE (:Person {name:'Elizabeth'})"); + db.executeTransactionally("CREATE (:Person {name:'Jack'})"); + db.executeTransactionally("CREATE (:Person {name:'Joy'})"); + db.executeTransactionally("CREATE (:Person {name:'Sarah'})"); + db.executeTransactionally("CREATE (:Person {name:'Jane'})"); + db.executeTransactionally("CREATE (:Person {name:'Jeff', surname:'Logan'})"); + db.executeTransactionally("CREATE (:City {name:'Milano'})"); + db.executeTransactionally("CREATE (:City {name:'Roma'})"); + db.executeTransactionally("CREATE (:City {name:'Firenze'})"); + db.executeTransactionally("CREATE (:City {name:'Taormina', region:'Sicilia'})"); + TestUtil.testResult(db, "CALL apoc.meta.data({sample:5})", (r) -> { + Map personNameProperty = r.next(); + Map personSurnameProperty = r.next(); + assertEquals("Person", personNameProperty.get("label")); + assertEquals("name", personNameProperty.get("property")); + assertEquals("Person", personSurnameProperty.get("label")); + assertEquals("surname", personSurnameProperty.get("property")); + + Map cityNameProperty = r.next(); + Map cityRegionProperty = r.next(); + assertEquals("City", cityNameProperty.get("label")); + assertEquals("name", cityNameProperty.get("property")); + assertEquals("City", cityRegionProperty.get("label")); + assertEquals("region", cityRegionProperty.get("property")); + }); + } + + @Test + public void testRelationshipAndNodeNames() { + db.executeTransactionally("CREATE (a:NODE)-[r:RELATIONSHIP]->(m:Movie)"); + TestUtil.testResult(db, "CALL apoc.meta.data()", (r) -> { + assertThat(r.stream().map(m -> m.get("label"))).contains("RELATIONSHIP", "NODE"); + r.close(); + }); + } + + @Test + public void testMetaDataWithSample5() { + db.executeTransactionally("create index for (n:Person) on (n.name)"); + db.executeTransactionally("CREATE (:Person {name:'John', surname:'Brown'})"); + db.executeTransactionally("CREATE (:Person {name:'Daisy', surname:'Bob'})"); + db.executeTransactionally("CREATE (:Person {name:'Nick'})"); + db.executeTransactionally("CREATE (:Person {name:'Jack', surname:'White'})"); + db.executeTransactionally("CREATE (:Person {name:'Elizabeth'})"); + db.executeTransactionally("CREATE (:Person {name:'Joy'})"); + db.executeTransactionally("CREATE (:Person {name:'Sarah', surname:'Taylor'})"); + db.executeTransactionally("CREATE (:Person {name:'Jane'})"); + db.executeTransactionally("CREATE (:Person {name:'Jeff', surname:'Logan'})"); + db.executeTransactionally("CREATE (:Person {name:'Tom'})"); + TestUtil.testResult(db, "CALL apoc.meta.data({sample:5})", (r) -> { + assertThat(r.stream().map(m -> m.get("property"))).contains("name"); + r.close(); + }); + } + + @Test + public void testSchemaWithSample() { + db.executeTransactionally("create constraint for (p:Person) require p.name is unique"); + db.executeTransactionally("CREATE (:Person {name:'Tom'})"); + db.executeTransactionally("CREATE (:Person {name:'John', surname:'Brown'})"); + db.executeTransactionally("CREATE (:Person {name:'Nick'})"); + db.executeTransactionally("CREATE (:Person {name:'Daisy', surname:'Bob'})"); + db.executeTransactionally("CREATE (:Person {name:'Elizabeth'})"); + db.executeTransactionally("CREATE (:Person {name:'Jack', surname:'White'})"); + db.executeTransactionally("CREATE (:Person {name:'Joy'})"); + db.executeTransactionally("CREATE (:Person {name:'Sarah', surname:'Taylor'})"); + db.executeTransactionally("CREATE (:Person {name:'Jane'})"); + db.executeTransactionally("CREATE (:Person {name:'Jeff', surname:'Logan'})"); + testCall(db, "CALL apoc.meta.schema({sample:2})", (row) -> { + Map o = (Map) row.get("value"); + assertEquals(1, o.size()); + + Map person = (Map) o.get("Person"); + Map personProperties = (Map) person.get("properties"); + Map personNameProperty = (Map) personProperties.get("name"); + Map personSurnameProperty = (Map) personProperties.get("surname"); + assertNotNull(person); + assertEquals("node", person.get("type")); + assertEquals(10L, person.get("count")); + assertEquals("STRING", personNameProperty.get("type")); + assertEquals(false, personSurnameProperty.get("unique")); + assertEquals("STRING", personSurnameProperty.get("type")); + assertEquals(2, personProperties.size()); + }); + } + + @Test + public void testSchemaWithSample5() { + db.executeTransactionally("create constraint for (p:Person) require p.name is unique"); + db.executeTransactionally("CREATE (:Person {name:'Tom'})"); + db.executeTransactionally("CREATE (:Person {name:'John', surname:'Brown'})"); + db.executeTransactionally("CREATE (:Person {name:'Nick'})"); + db.executeTransactionally("CREATE (:Person {name:'Daisy', surname:'Bob'})"); + db.executeTransactionally("CREATE (:Person {name:'Elizabeth'})"); + db.executeTransactionally("CREATE (:Person {name:'Jack', surname:'White'})"); + db.executeTransactionally("CREATE (:Person {name:'Joy'})"); + db.executeTransactionally("CREATE (:Person {name:'Sarah', surname:'Taylor'})"); + db.executeTransactionally("CREATE (:Person {name:'Jane'})"); + db.executeTransactionally("CREATE (:Person {name:'Jeff', surname:'Logan'})"); + testCall(db, "CALL apoc.meta.schema({sample:5})", (row) -> { + Map o = (Map) row.get("value"); + assertEquals(1, o.size()); + Map person = (Map) o.get("Person"); + Map personProperties = (Map) person.get("properties"); + Map personNameProperty = (Map) personProperties.get("name"); + assertNotNull(person); + assertEquals("node", person.get("type")); + assertEquals(10L, person.get("count")); + assertEquals("STRING", personNameProperty.get("type")); + assertEquals(true, personNameProperty.get("unique")); + assertTrue(personProperties.size() >= 1); + }); + } + + @Test + public void testMetaGraphExtraRelsWithSample() { + db.executeTransactionally("CREATE (:S1 {name:'Tom'})"); + db.executeTransactionally("CREATE (:S2 {name:'John', surname:'Brown'})-[:KNOWS{since:2012}]->(:S7)"); + db.executeTransactionally("CREATE (:S1 {name:'Nick'})"); + db.executeTransactionally("CREATE (:S3 {name:'Daisy', surname:'Bob'})-[:KNOWS{since:2012}]->(:S7)"); + db.executeTransactionally("CREATE (:S1 {name:'Elizabeth'})"); + db.executeTransactionally("CREATE (:S4 {name:'Jack', surname:'White'})-[:KNOWS{since:2012}]->(:S7)"); + db.executeTransactionally("CREATE (:S1 {name:'Joy'})"); + db.executeTransactionally("CREATE (:S5 {name:'Sarah', surname:'Taylor'})-[:KNOWS{since:2012}]->(:S7)"); + db.executeTransactionally("CREATE (:S1 {name:'Jane'})"); + db.executeTransactionally("CREATE (:S6 {name:'Jeff', surname:'Logan'})-[:KNOWS{since:2012}]->(:S7)"); + + testCall(db, "call apoc.meta.graph({sample:2})", (row) -> { + List nodes = (List) row.get("nodes"); + assertEquals(7, nodes.size()); + }); + } + + // Tests for T4L + + @Test + public void testRelTypePropertiesBasic() { + db.executeTransactionally("CREATE (:Base)-[:RELTYPE { a: 1, d: null }]->(:Target)"); + db.executeTransactionally("CREATE (:Base)-[:RELTYPE { a: 2, b: 2, c: 2, d: 4 }]->(:Target);"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties()", r -> { + List> records = gatherRecords(r); + + assertTrue(hasRecordMatching( + records, + m -> m.get("propertyName").equals("a") + && ((List) m.get("propertyTypes")).get(0).equals("Long") + && m.get("mandatory").equals(false))); + + assertTrue(hasRecordMatching( + records, + m -> m.get("propertyName").equals("b") + && ((List) m.get("propertyTypes")).get(0).equals("Long") + && m.get("mandatory").equals(false))); + + assertTrue(hasRecordMatching( + records, + m -> m.get("propertyName").equals("c") + && ((List) m.get("propertyTypes")).get(0).equals("Long") + && m.get("mandatory").equals(false))); + + assertTrue(hasRecordMatching( + records, + m -> m.get("propertyName").equals("d") + && ((List) m.get("propertyTypes")).get(0).equals("Long") + && m.get("mandatory").equals(false))); + }); + } + + @Test + public void testRelTypePropertiesIncludes() { + db.executeTransactionally("CREATE (:A)-[:CATCHME { c: 1 }]->(:B)"); + db.executeTransactionally("CREATE (:A)-[:IGNOREME { d: 1 }]->(:B)"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties({ includeRels: ['CATCHME'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + assertTrue(records.get(0).get("propertyName").equals("c")); + }); + } + + @Test + public void testRelTypePropertiesIncludesWithDoubleRel() { + db.executeTransactionally("CREATE (a:A)-[:FIRST_A {a: 1}]->(b:B), (a)-[:SECOND_B {b: '2'}]->(c)"); + db.executeTransactionally( + "CREATE (:Alpha)-[:FIRST_A {c: true}]->(:Beta), (:Gamma)-[:SECOND_B {d: datetime()}]->(:Delta)"); + + final Consumer assertFirstRel = res -> { + Map r = res.next(); + assertEquals("a", r.get("propertyName")); + assertEquals(":`FIRST_A`", r.get("relType")); + assertEquals(List.of("Long"), r.get("propertyTypes")); + r = res.next(); + assertEquals("c", r.get("propertyName")); + assertEquals(":`FIRST_A`", r.get("relType")); + assertEquals(List.of("Boolean"), r.get("propertyTypes")); + assertFalse(res.hasNext()); + }; + + final Consumer assertSecondRel = res -> { + Map r = res.next(); + assertEquals("b", r.get("propertyName")); + assertEquals(":`SECOND_B`", r.get("relType")); + assertEquals(List.of("String"), r.get("propertyTypes")); + r = res.next(); + assertEquals("d", r.get("propertyName")); + assertEquals(":`SECOND_B`", r.get("relType")); + assertEquals(List.of("DateTime"), r.get("propertyTypes")); + assertFalse(res.hasNext()); + }; + + final String query = + "CALL apoc.meta.relTypeProperties($conf) YIELD propertyName, relType, propertyTypes RETURN * ORDER BY relType"; + + testResult(db, query, map("conf", map("includeRels", List.of("FIRST_A"))), assertFirstRel); + testResult(db, query, map("conf", map("excludeRels", List.of("SECOND_B"))), assertFirstRel); + + testResult(db, query, map("conf", map("excludeRels", List.of("FIRST_A"))), assertSecondRel); + testResult(db, query, map("conf", map("includeRels", List.of("SECOND_B"))), assertSecondRel); + + TestUtil.testCallCount(db, "CALL apoc.meta.relTypeProperties()", emptyMap(), 4); + } + + @Test + public void testNodeTypePropertiesNodeExcludes() { + db.executeTransactionally("CREATE (:ExcludeMe)"); + db.executeTransactionally("CREATE (:IncludeMe)"); + + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties({ excludeLabels: ['ExcludeMe'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + assertEquals(":`IncludeMe`", records.get(0).get("nodeType")); + }); + } + + @Test + public void testNodeTypePropertiesNodeIncludes() { + db.executeTransactionally("CREATE (:ExcludeMe)"); + db.executeTransactionally("CREATE (:IncludeMe)"); + + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties({ includeLabels: ['IncludeMe'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + assertEquals(":`IncludeMe`", records.get(0).get("nodeType")); + }); + } + + @Test + public void testNodeTypePropertiesRelExcludes() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties({ excludeRels: ['RELA'] })", r -> { + List> records = gatherRecords(r); + assertEquals(2, records.size()); + for (Map rec : records) { + if (rec.get("nodeType").equals(":`A`")) { + assertEquals(":`B`", rec.get("nodeType")); + } + if (rec.get("nodeType").equals(":`C`")) { + assertEquals(":`D`", rec.get("nodeType")); + } + } + }); + } + + @Test + public void testNodeTypePropertiesRelIncludes() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties({ includeRels: ['RELA'] })", r -> { + List> records = gatherRecords(r); + assertEquals(2, records.size()); + for (Map rec : records) { + if (rec.get("nodeType").equals(":`A`")) { + assertEquals(":`A`", rec.get("nodeType")); + } + if (rec.get("nodeType").equals(":`C`")) { + assertEquals(":`C`", rec.get("nodeType")); + } + } + }); + } + + @Test + public void testNodeTypePropertiesWithWeirdConfig() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult( + db, "CALL apoc.meta.nodeTypeProperties({ includeRels: ['RELA'], stupidInput: ['RELB'] })", r -> { + // should ignore all unknown input{ + List> records = gatherRecords(r); + assertEquals(2, records.size()); + for (Map rec : records) { + if (rec.get("nodeType").equals(":`A`")) { + assertEquals(":`A`", rec.get("nodeType")); + } + if (rec.get("nodeType").equals(":`C`")) { + assertEquals(":`C`", rec.get("nodeType")); + } + } + }); + } + + @Test + public void testRelTypePropertiesWithWeirdConfig() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties({ includeLabels: ['A'], stupidInput: ['B'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + if (rec.get("relType").equals(":`RELA`")) { + assertEquals(":`RELA`", rec.get("relType")); + } + } + }); + } + + @Test + public void testRelTypePropertiesRelExcludes() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties({ excludeRels: ['RELA'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + assertEquals(":`RELB`", records.get(0).get("relType")); + }); + } + + @Test + public void testRelTypePropertiesRelIncludes() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties({ includeRels: ['RELA'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + assertEquals(":`RELA`", records.get(0).get("relType")); + }); + } + + @Test + public void testRelTypePropertiesNodeExcludes() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties({ excludeLabels: ['A'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + if (rec.get("relType").equals(":`RELB`")) { + assertEquals(":`RELB`", rec.get("relType")); + } + } + }); + } + + @Test + public void testRelTypePropertiesNodeIncludes() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties({ includeLabels: ['A'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + if (rec.get("relType").equals(":`RELA`")) { + assertEquals(":`RELA`", rec.get("relType")); + } + } + }); + } + + @Test + public void testNodeTypePropertiesNodeIncludesRelIncludes1() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + // both together contract the data model and it should result in 0 results + TestUtil.testResult( + db, + "CALL apoc.meta.nodeTypeProperties({ includeLabels: ['A'], includeRels: ['RELB'] })", + r -> assertEquals(0, gatherRecords(r).size())); + } + + @Test + public void testNodeTypePropertiesNodeIncludesRelIncludes2() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult( + db, "CALL apoc.meta.nodeTypeProperties({ includeLabels: ['A'], includeRels: ['RELA'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); // why not A and C? The label has to be on the start of the rel + for (Map rec : records) { + if (rec.get("nodeType").equals(":`A`")) { + assertEquals(":`A`", rec.get("nodeType")); + } + } + }); + } + + @Test + public void testRelTypePropertiesNodeIncludesAndRelsInclude1() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + // both together contract the data model and it should result in 0 results + TestUtil.testResult( + db, "CALL apoc.meta.relTypeProperties({ includeLabels: ['A'], includeRels: ['RELB'] })", r -> { + assertEquals(0, gatherRecords(r).size()); + }); + } + + @Test + public void testRelTypePropertiesNodeIncludesAndRelsInclude2() { + db.executeTransactionally("CREATE (:A)-[:RELA { x: 1 }]->(:C)"); + db.executeTransactionally("CREATE (:B)-[:RELB { x: 1 }]->(:D)"); + + TestUtil.testResult( + db, "CALL apoc.meta.relTypeProperties({ includeLabels: ['A'], includeRels: ['RELA'] })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + if (rec.get("relType").equals(":`RELA`")) { + assertEquals(":`RELA`", rec.get("relType")); + } + } + }); + } + + @Test + public void testNodeTypePropertiesCompleteResult() { + db.executeTransactionally("CREATE (:Foo { z: 'hej' });"); + db.executeTransactionally("CREATE (:Foo { z: 1 });"); + + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties()", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + assertEquals(":`Foo`", rec.get("nodeType")); + assertEquals(List.of("Foo"), rec.get("nodeLabels")); + assertEquals("z", rec.get("propertyName")); + assertEquals(List.of("Long", "String"), rec.get("propertyTypes")); + assertEquals(2L, rec.get("propertyObservations")); + assertEquals(2L, rec.get("totalObservations")); + assertEquals(false, rec.get("mandatory")); + } + }); + } + + @Test + public void testVectorTypesOnProperties() { + var vectorTypes = List.of("INT64", "INT32", "INT16", "INT8", "FLOAT64", "FLOAT32"); + for (String type : vectorTypes) { + db.executeTransactionally("CYPHER 25 CREATE (:Foo { z: VECTOR([1, 2, 3], 3, %s) });".formatted(type)); + } + + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties()", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + assertEquals(":`Foo`", rec.get("nodeType")); + assertEquals(List.of("Foo"), rec.get("nodeLabels")); + assertEquals("z", rec.get("propertyName")); + assertEquals( + List.of( + "Float32Vector", + "Float64Vector", + "Int16Vector", + "Int32Vector", + "Int64Vector", + "Int8Vector"), + rec.get("propertyTypes")); + assertEquals(6L, rec.get("propertyObservations")); + assertEquals(6L, rec.get("totalObservations")); + assertEquals(false, rec.get("mandatory")); + } + }); + + for (String type : vectorTypes) { + db.executeTransactionally( + "CYPHER 25 CREATE (:A)-[:Foo { z: VECTOR([1, 2, 3], 3, %s) }]->(:B);".formatted(type)); + } + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties()", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + assertEquals(":`Foo`", rec.get("relType")); + assertEquals(List.of("A"), rec.get("sourceNodeLabels")); + assertEquals(List.of("B"), rec.get("targetNodeLabels")); + assertEquals("z", rec.get("propertyName")); + assertEquals( + List.of( + "Float32Vector", + "Float64Vector", + "Int16Vector", + "Int32Vector", + "Int64Vector", + "Int8Vector"), + rec.get("propertyTypes")); + assertEquals(6L, rec.get("propertyObservations")); + assertEquals(6L, rec.get("totalObservations")); + assertEquals(false, rec.get("mandatory")); + } + }); + } + + @Test + public void testRelTypePropertiesCompleteResult() { + db.executeTransactionally("CREATE (:A)-[:Foo { z: 'hej' }]->(:B);"); + db.executeTransactionally("CREATE (:A)-[:Foo { z: 1 }]->(:B);"); + + TestUtil.testResult(db, "CALL apoc.meta.relTypeProperties()", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + assertEquals(":`Foo`", rec.get("relType")); + assertEquals(List.of("A"), rec.get("sourceNodeLabels")); + assertEquals(List.of("B"), rec.get("targetNodeLabels")); + assertEquals("z", rec.get("propertyName")); + assertEquals(List.of("Long", "String"), rec.get("propertyTypes")); + assertEquals(2L, rec.get("propertyObservations")); + assertEquals(2L, rec.get("totalObservations")); + assertEquals(false, rec.get("mandatory")); + } + }); + } + + @Test + public void testNodeTypePropertiesWithSpecialSampleSize() { + db.executeTransactionally("CREATE (:Foo { z: 'hej' });"); + db.executeTransactionally("CREATE (:Foo { z: 1 });"); + db.executeTransactionally("CREATE (:Foo { z: true });"); + db.executeTransactionally("CREATE (:Foo { z: 1.5 });"); + + // sample = -1 scans all entities + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties({ pollingPeriod: -1})", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + assertEquals(":`Foo`", rec.get("nodeType")); + assertEquals("z", rec.get("propertyName")); + assertEquals(4L, rec.get("propertyObservations")); + assertEquals(4L, rec.get("totalObservations")); + } + }); + + // not scan all of them, might not reach maxSampleSize + TestUtil.testResult(db, "CALL apoc.meta.nodeTypeProperties({ pollingPeriod: 1 })", r -> { + List> records = gatherRecords(r); + assertEquals(1, records.size()); + for (Map rec : records) { + assertEquals(":`Foo`", rec.get("nodeType")); + assertEquals("z", rec.get("propertyName")); + assertTrue((long) rec.get("propertyObservations") <= 4L); + assertTrue((long) rec.get("totalObservations") <= 4L); + } + }); + } + + @Test + public void testNodeTypePropertiesEquivalenceAdvanced() { + db.executeTransactionally("CREATE (:Foo { l: 1, s: 'foo', d: datetime(), ll: ['a', 'b'], dl: [2.0, 3.0] });"); + // Missing all properties to make everything non-mandatory. + db.executeTransactionally("CREATE (:Foo { z: 1 });"); + assertTrue(testDBCallEquivalence( + db, "CALL apoc.meta.nodeTypeProperties()", "CYPHER 5 CALL db.schema.nodeTypeProperties()")); + } + + @Test + public void testRelTypePropertiesEquivalenceAdvanced() { + db.executeTransactionally( + "CREATE (:Foo)-[:REL { l: 1, s: 'foo', d: datetime(), ll: ['a', 'b'], dl: [2.0, 3.0] }]->();"); + // Missing all properties to make everything non-mandatory. + db.executeTransactionally("CREATE (:Foo)-[:REL { z: 1 }]->();"); + assertTrue(testDBCallEquivalence( + db, "CALL apoc.meta.relTypeProperties()", "CYPHER 5 CALL db.schema.relTypeProperties()")); + } + + @Test + public void testNodeTypePropertiesEquivalenceTypeMapping() { + String q = "CREATE (:Test {" + " longProp: 1," + + " doubleProp: 3.14," + + " stringProp: 'Hello'," + + " longArrProp: [1,2,3]," + + " doubleArrProp: [3.14, 3.14]," + + " stringArrProp: ['Hello', 'World']," + + " dateTimeProp: datetime()," + + " dateProp: date()," + + " pointProp: point({ x:0, y:4, z:1 })," + + " pointArrProp: [point({ x:0, y:4, z:1 }), point({ x:0, y:4, z:1 })]," + + " boolProp: true," + + " boolArrProp: [true, false]\n" + + "})" + + "CREATE (:Test { randomProp: 'this property is here to make everything mandatory = false'});"; + + db.executeTransactionally(q); + assertTrue(testDBCallEquivalence( + db, "CALL apoc.meta.nodeTypeProperties()", "CYPHER 5 CALL db.schema.nodeTypeProperties()")); + } + + @Test + public void testRelTypePropertiesEquivalenceTypeMapping() { + String q = "CREATE (t:Test)-[:REL{" + " longProp: 1," + + " doubleProp: 3.14," + + " stringProp: 'Hello'," + + " longArrProp: [1,2,3]," + + " doubleArrProp: [3.14, 3.14]," + + " stringArrProp: ['Hello', 'World']," + + " dateTimeProp: datetime()," + + " dateProp: date()," + + " pointProp: point({ x:0, y:4, z:1 })," + + " pointArrProp: [point({ x:0, y:4, z:1 }), point({ x:0, y:4, z:1 })]," + + " boolProp: true," + + " boolArrProp: [true, false]\n" + + "}]->(t)" + + "CREATE (b:Test)-[:REL{ randomProp: 'this property is here to make everything mandatory = false'}]->(b);"; + + db.executeTransactionally(q); + assertTrue(testDBCallEquivalence( + db, "CALL apoc.meta.relTypeProperties()", "CYPHER 5 CALL db.schema.relTypeProperties()")); + } + + @Test + public void testMetaDataOf() { + db.executeTransactionally("create index for (n:Movie) on (n.title)"); + db.executeTransactionally("create constraint for (a:Actor) require a.name is unique"); + db.executeTransactionally( + "CREATE (p:Person {name:'Tom Hanks'}), (m:Movie {title:'Forrest Gump'}), (pr:Product{name: 'Awesome Product'}), " + + "(p)-[:VIEWED]->(m), (p)-[:BOUGHT{quantity: 10}]->(pr)"); + Set> expectedResult = new HashSet<>(); + expectedResult.add(MapUtil.map( + "other", + List.of(), + "count", + 0L, + "existence", + false, + "index", + false, + "label", + "BOUGHT", + "right", + 0L, + "type", + "INTEGER", + "sample", + null, + "array", + false, + "left", + 0L, + "unique", + false, + "property", + "quantity", + "elementType", + "relationship", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of(), + "count", + 0L, + "existence", + false, + "index", + false, + "label", + "Product", + "right", + 0L, + "type", + "STRING", + "sample", + null, + "array", + false, + "left", + 0L, + "unique", + false, + "property", + "name", + "elementType", + "node", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of("Product"), + "count", + 1L, + "existence", + false, + "index", + false, + "label", + "BOUGHT", + "right", + 0L, + "type", + "RELATIONSHIP", + "sample", + null, + "array", + false, + "left", + 1L, + "unique", + false, + "property", + "Person", + "elementType", + "relationship", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of("Product"), + "count", + 1L, + "existence", + false, + "index", + false, + "label", + "Person", + "right", + 0L, + "type", + "RELATIONSHIP", + "sample", + null, + "array", + false, + "left", + 1L, + "unique", + false, + "property", + "BOUGHT", + "elementType", + "node", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of(), + "count", + 0L, + "existence", + false, + "index", + false, + "label", + "Person", + "right", + 0L, + "type", + "STRING", + "sample", + null, + "array", + false, + "left", + 0L, + "unique", + false, + "property", + "name", + "elementType", + "node", + "otherLabels", + List.of())); + + String keys = expectedResult.stream() + .findAny() + .map(Map::keySet) + .map(s -> String.join(", ", s)) + .get(); + + Consumer assertResult = (r) -> { + Set> result = r.stream().collect(Collectors.toSet()); + assertEquals(expectedResult, result); + }; + + TestUtil.testResult(db, "CALL apoc.meta.data.of('MATCH p = ()-[:BOUGHT]->() RETURN p')", assertResult); + + TestUtil.testResult( + db, + "MATCH p = ()-[:BOUGHT]->() " + "WITH {nodes: nodes(p), relationships: relationships(p)} AS graphMap " + + String.format("CALL apoc.meta.data.of(graphMap) YIELD %s ", keys) + + "RETURN " + + keys, + assertResult); + + TestUtil.testResult( + db, + "CALL apoc.graph.fromCypher('MATCH p = ()-[:BOUGHT]->() RETURN p', {}, '', {}) YIELD graph " + + String.format("CALL apoc.meta.data.of(graph) YIELD %s ", keys) + + "RETURN " + + keys, + assertResult); + } + + @Test + public void testMetaDataOfWithRelConstraints() { + db.executeTransactionally("CREATE CONSTRAINT FOR ()-[like:LIKES]-() REQUIRE like.score IS UNIQUE"); + db.executeTransactionally( + "CREATE (gem:Person {name: \"Gem\"})-[:LIKES {score: 10}]->(cake:Cake {type: \"Chocolate\"})"); + Set> expectedResult = new HashSet<>(); + expectedResult.add(MapUtil.map( + "other", + List.of("Cake"), + "count", + 1L, + "existence", + false, + "index", + false, + "label", + "LIKES", + "right", + 0L, + "type", + "RELATIONSHIP", + "sample", + null, + "array", + false, + "left", + 1L, + "unique", + false, + "property", + "Person", + "elementType", + "relationship", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of(), + "count", + 0L, + "existence", + false, + "index", + true, + "label", + "LIKES", + "right", + 0L, + "type", + "INTEGER", + "sample", + null, + "array", + false, + "left", + 0L, + "unique", + true, + "property", + "score", + "elementType", + "relationship", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of(), + "count", + 0L, + "existence", + false, + "index", + false, + "label", + "Cake", + "right", + 0L, + "type", + "STRING", + "sample", + null, + "array", + false, + "left", + 0L, + "unique", + false, + "property", + "type", + "elementType", + "node", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of("Cake"), + "count", + 1L, + "existence", + false, + "index", + false, + "label", + "Person", + "right", + 0L, + "type", + "RELATIONSHIP", + "sample", + null, + "array", + false, + "left", + 1L, + "unique", + false, + "property", + "LIKES", + "elementType", + "node", + "otherLabels", + List.of())); + expectedResult.add(MapUtil.map( + "other", + List.of(), + "count", + 0L, + "existence", + false, + "index", + false, + "label", + "Person", + "right", + 0L, + "type", + "STRING", + "sample", + null, + "array", + false, + "left", + 0L, + "unique", + false, + "property", + "name", + "elementType", + "node", + "otherLabels", + List.of())); + + Set> actualResult = new HashSet<>(); + + TestUtil.testResult(db, "CALL apoc.meta.data.of('MATCH p = ()-[:LIKES]->() RETURN p')", result -> { + while (result.hasNext()) { + actualResult.add(result.next()); + } + assertEquals(actualResult, expectedResult); + }); + } + + @Test + public void testMetaGraphOf() { + db.executeTransactionally( + "CREATE (p:Person {name:'Tom Hanks'}), (m:Movie {title:'Forrest Gump'}), (pr:Product{name: 'Awesome Product'}), " + + "(p)-[:VIEWED]->(m), (p)-[:BOUGHT{quantity: 10}]->(pr)"); + + Consumer assertResult = (r) -> { + Map row = r.next(); + List nodes = (List) row.get("nodes"); + List relationships = (List) row.get("relationships"); + assertEquals(2, nodes.size()); + assertEquals(1, relationships.size()); + Set> labels = nodes.stream() + .map(n -> StreamSupport.stream(n.getLabels().spliterator(), false) + .map(Label::name) + .collect(Collectors.toSet())) + .collect(Collectors.toSet()); + assertEquals(2, labels.size()); + assertEquals(Set.of(Set.of("Person"), Set.of("Product")), labels); + assertEquals( + RelationshipType.withName("BOUGHT"), relationships.get(0).getType()); + }; + + TestUtil.testResult(db, "CALL apoc.meta.graph.of('MATCH p = ()-[:BOUGHT]->() RETURN p')", assertResult); + + TestUtil.testResult( + db, + "MATCH p = ()-[:BOUGHT]->() " + "WITH {nodes: nodes(p), relationships: relationships(p)} AS graphMap " + + "CALL apoc.meta.graph.of(graphMap) YIELD nodes, relationships " + + "RETURN *", + assertResult); + + TestUtil.testResult( + db, + "CALL apoc.graph.fromCypher('MATCH p = ()-[:BOUGHT]->() RETURN p', {}, '', {}) YIELD graph " + + "CALL apoc.meta.graph.of(graph) YIELD nodes, relationships " + + "RETURN *", + assertResult); + } + + @Test + public void testMetaRelTypePropertiesWithManyRels() { + db.executeTransactionally("UNWIND range (0, 200) as idx CREATE (a:A)-[:FIRST_A]-> (b:B)"); + db.executeTransactionally("CREATE (a:A)-[:FIRST_A {a: 1}]->(b:B)"); + + // with default maxRels + testCall(db, "CALL apoc.meta.relTypeProperties({includeRels: ['FIRST_A']})", r -> { + assertNull(r.get("propertyTypes")); + assertNull(r.get("propertyName")); + }); + + // with maxRels incremented + testCall(db, "CALL apoc.meta.relTypeProperties({includeRels: ['FIRST_A'], maxRels: 1000})", r -> { + assertEquals(List.of("Long"), r.get("propertyTypes")); + assertEquals("a", r.get("propertyName")); + }); + } + + @Test + public void testMetaStatsWithTwoDots() { + db.executeTransactionally( + "CREATE (n:`My:Label` {id:1})-[r:`http://www.w3.org/2000/01/rdf-schema#isDefinedBy` {alpha: 'beta'}]->(s:Another)"); + TestUtil.testCall(db, "CALL apoc.meta.stats()", row -> { + assertEquals(map("My:Label", 1L, "Another", 1L), row.get("labels")); + assertEquals(2L, row.get("labelCount")); + assertEquals(map("http://www.w3.org/2000/01/rdf-schema#isDefinedBy", 1L), row.get("relTypesCount")); + assertEquals(2L, row.get("propertyKeyCount")); + assertEquals( + map( + "()-[:http://www.w3.org/2000/01/rdf-schema#isDefinedBy]->(:Another)", + 1L, + "()-[:http://www.w3.org/2000/01/rdf-schema#isDefinedBy]->()", + 1L, + "(:My:Label)-[:http://www.w3.org/2000/01/rdf-schema#isDefinedBy]->()", + 1L), + row.get("relTypes")); + }); + } + + @Test + public void testMetaDataWithRelIndexes() { + datasetWithNodeRelIdxs(); + + testResult( + db, + "CALL apoc.meta.data() YIELD label, property, index, type " + + "\nWHERE type='STRING' RETURN label, property, index ORDER BY property", + (res) -> { + Map aProp = res.next(); + assertEquals("Person", aProp.get("label")); + assertEquals("a", aProp.get("property")); + assertFalse((boolean) aProp.get("index")); + + Map bProp = res.next(); + assertEquals("Movie", bProp.get("label")); + assertEquals("b", bProp.get("property")); + assertTrue((boolean) bProp.get("index")); + + Map fooProp = res.next(); + assertEquals("ACTED_IN", fooProp.get("label")); + assertEquals("foo", fooProp.get("property")); + assertFalse((boolean) fooProp.get("index")); + + Map idProp = res.next(); + assertEquals("ACTED_IN", idProp.get("label")); + assertEquals("id", idProp.get("property")); + assertTrue((boolean) idProp.get("index")); + + Map rolesProp = res.next(); + assertEquals("ACTED_IN", rolesProp.get("label")); + assertEquals("roles", rolesProp.get("property")); + assertTrue((boolean) rolesProp.get("index")); + assertFalse(res.hasNext()); + }); + } + + @Test + public void testMetaSchemaWithRelIndexes() { + datasetWithNodeRelIdxs(); + + TestUtil.testCall(db, "CALL apoc.meta.schema()", (row) -> { + Map value = (Map) row.get("value"); + Map relData = (Map) value.get("ACTED_IN"); + Map relProperties = (Map) relData.get("properties"); + Map rolesProp = (Map) relProperties.get("roles"); + assertTrue((boolean) rolesProp.get("indexed")); + Map fooProp = (Map) relProperties.get("foo"); + assertFalse((boolean) fooProp.get("indexed")); + Map idProp = (Map) relProperties.get("id"); + assertTrue((boolean) idProp.get("indexed")); + + Map movieData = (Map) value.get("Movie"); + Map movieProperties = (Map) movieData.get("properties"); + Map bProp = (Map) movieProperties.get("b"); + assertTrue((boolean) bProp.get("indexed")); + + Map personData = (Map) value.get("Person"); + Map personProperties = (Map) personData.get("properties"); + Map aProp = (Map) personProperties.get("a"); + assertFalse((boolean) aProp.get("indexed")); + }); + } + + private void datasetWithNodeRelIdxs() { + db.executeTransactionally("CREATE INDEX node_index_name FOR (n:Movie) ON (n.b)"); + db.executeTransactionally("CREATE INDEX rel_index_name FOR ()-[r:ACTED_IN]-() ON (r.roles, r.id)"); + db.executeTransactionally( + "CREATE (:Person {a: '11'})-[:ACTED_IN {roles:'Forrest', id:'123', foo: 'bar'}]->(:Movie {b: '1'})"); + } + + @Test + public void testMetaStatsWithLabelAndRelTypeCountInUse() { + db.executeTransactionally("CREATE (:Node:Test)-[:REL {a: 'b'}]->(:Node {c: 'd'})<-[:REL]-(:Node:Test)"); + db.executeTransactionally("CREATE (:A {e: 'f'})-[:ANOTHER {g: 'h'}]->(:C)"); + + TestUtil.testCall(db, "CALL apoc.meta.stats()", row -> { + assertEquals(map("A", 1L, "C", 1L, "Test", 2L, "Node", 3L), row.get("labels")); + assertEquals(5L, row.get("nodeCount")); + assertEquals(4L, row.get("labelCount")); + + assertEquals(map("REL", 2L, "ANOTHER", 1L), row.get("relTypesCount")); + assertEquals(2L, row.get("relTypeCount")); + assertEquals(3L, row.get("relCount")); + Map expectedRelTypes = map( + "(:A)-[:ANOTHER]->()", + 1L, + "()-[:REL]->(:Node)", + 2L, + "(:Test)-[:REL]->()", + 2L, + "(:Node)-[:REL]->()", + 2L, + "()-[:ANOTHER]->(:C)", + 1L, + "()-[:ANOTHER]->()", + 1L, + "()-[:REL]->()", + 2L); + assertEquals(expectedRelTypes, row.get("relTypes")); + }); + + db.executeTransactionally("match p=(:A)-[:ANOTHER]->(:C) delete p"); + TestUtil.testCall(db, "CALL apoc.meta.stats()", row -> { + assertEquals(map("Test", 2L, "Node", 3L), row.get("labels")); + assertEquals(3L, row.get("nodeCount")); + assertEquals(2L, row.get("labelCount")); + + assertEquals(map("REL", 2L), row.get("relTypesCount")); + assertEquals(1L, row.get("relTypeCount")); + assertEquals(2L, row.get("relCount")); + Map expectedRelTypes = map( + "()-[:REL]->(:Node)", 2L, "(:Test)-[:REL]->()", 2L, "(:Node)-[:REL]->()", 2L, "()-[:REL]->()", 2L); + assertEquals(expectedRelTypes, row.get("relTypes")); + }); + } + + @Test + public void testMetaNodesCount() { + db.executeTransactionally("CREATE (:MyCountLabel {id: 1}), (:MyCountLabel {id: 2}), (:ThirdLabel {id: 3})"); + + // 2 outcome rels and 1 incoming + db.executeTransactionally( + "MATCH (n:MyCountLabel {id: 1}), (m:ThirdLabel {id: 3}) " + + "CREATE (n)-[:MY_COUNT_REL]->(m), (n)-[:ANOTHER_MY_COUNT_REL]->(m), (n)<-[:ANOTHER_MY_COUNT_REL]-(m)"); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel'], {rels: ['MY_COUNT_REL']}) AS count", + row -> assertEquals(1L, row.get("count"))); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel', 'NotExistent'], {rels: ['MY_COUNT_REL']}) AS count", + row -> assertEquals(1L, row.get("count"))); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel'], {rels: ['MY_COUNT_REL>']}) AS count", + row -> assertEquals(1L, row.get("count"))); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel'], {rels: ['MY_COUNT_REL<']}) AS count", + row -> assertEquals(0L, row.get("count"))); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel'], {rels: ['MY_COUNT_REL', 'ANOTHER_MY_COUNT_REL']}) AS count", + row -> assertEquals(1L, row.get("count"))); + + // another 2 nodes with 2 new labels + db.executeTransactionally("CREATE (:AnotherCountLabel)<-[:MY_COUNT_REL]-(:NotInCountLabel)"); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel', 'AnotherCountLabel'], {rels: ['MY_COUNT_REL', 'ANOTHER_MY_COUNT_REL']}) AS count", + row -> assertEquals(2L, row.get("count"))); + + // create another 2 rels in `MyCountLabel` nodes + db.executeTransactionally("MATCH (n:MyCountLabel) WITH n CREATE (n)<-[:MY_COUNT_REL]-(:NotInCountLabel)"); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel', 'AnotherCountLabel'], {rels: ['MY_COUNT_REL', 'ANOTHER_MY_COUNT_REL']}) AS count", + row -> assertEquals(3L, row.get("count"))); + + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel', 'AnotherCountLabel'], {rels: ['MY_COUNT_REL', 'ANOTHER_MY_COUNT_REL']}) AS count", + row -> assertEquals(3L, row.get("count"))); + + // just to check that with both direction takes all + TestUtil.testCall( + db, + "RETURN apoc.meta.nodes.count(['MyCountLabel', 'AnotherCountLabel'], {rels: ['MY_COUNT_REL>', 'MY_COUNT_REL<', 'ANOTHER_MY_COUNT_REL']}) AS count", + row -> assertEquals(3L, row.get("count"))); + } + + @Test + public void testRelTypePropertiesMovies() throws Exception { + final String query = IOUtils.toString(new InputStreamReader( + Thread.currentThread().getContextClassLoader().getResourceAsStream("movies.cypher"))); + + db.executeTransactionally(query); + + TestUtil.testResult( + db, + "CALL apoc.meta.relTypeProperties($config)", + Map.of("config", Map.of("includeRels", List.of("REVIEWED"))), + r -> { + final Set> actual = r.stream().collect(Collectors.toSet()); + final Set> expected = Set.of( + Map.of( + "relType", + ":`REVIEWED`", + "sourceNodeLabels", + List.of("Person"), + "targetNodeLabels", + List.of("Movie"), + "propertyTypes", + List.of("Long"), + "mandatory", + false, + "propertyObservations", + 8L, + "totalObservations", + 8L, + "propertyName", + "rating"), + Map.of( + "relType", + ":`REVIEWED`", + "sourceNodeLabels", + List.of("Person"), + "targetNodeLabels", + List.of("Movie"), + "propertyTypes", + List.of("String"), + "mandatory", + false, + "propertyObservations", + 8L, + "totalObservations", + 8L, + "propertyName", + "summary")); + Assert.assertEquals(expected, actual); + }); + } + + @Test + public void testMetaGraphSampling() { + db.executeTransactionally("CREATE (:A)-[:R1]->(:B)-[:R1]->(:C)"); + + // Not specifying sampling will check through all relationships and make sure they + // exist, clearing out non-existing ones + testCall(db, "CALL apoc.meta.graph()", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(2, relationships.size()); + }); + + // A SampleSize larger than the number of Nodes will check one node still + testCall(db, "CALL apoc.meta.graph({ sample: 1000 })", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(2, relationships.size()); + }); + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } + + @Test + public void testMetaGraphSparseSampling() { + // The 3 procedures using this sampling, set to look at the whole graph + List samplingBasedProcs = List.of( + "apoc.meta.graph(", "apoc.meta.graph.of(\"MATCH p = ()-[]->() RETURN p\", ", "apoc.meta.subGraph("); + // The "Schema" Should be: A->B->C and A->C + for (int i = 0; i < 100; i++) { + if (i == 50) { + // Create one existing A-->C relationship + db.executeTransactionally("CREATE (:A)-[:R1]->(:C)"); + } else { + // Create 99 A->B->C relationships + db.executeTransactionally("CREATE (:A)-[:R1]->(:B)-[:R1]->(:C)"); + } + } + + for (String samplingProc : samplingBasedProcs) { + // Not specifying sampling will check through all relationships and make sure they + // exist, clearing out non-existing ones + testCall(db, "CALL " + samplingProc + " {})", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(3, relationships.size()); + }); + + // A SampleSize larger than the number of Nodes will check one node still, returning + // missing relationships. In this case; A->C which does exist will not be returned. + testCall(db, "CALL " + samplingProc + " { sample: 1000 })", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(2, relationships.size()); + }); + + // A SampleSize which isn't fine-grained enough to find the one A->C relationship, returning + // missing relationships. In this case; A->C which does exist will not be returned. + testCall(db, "CALL " + samplingProc + " { sample: 99 })", (row) -> { + List relationships = (List) row.get("relationships"); + assertEquals(2, relationships.size()); + }); + } + + db.executeTransactionally("MATCH (n) DETACH DELETE n"); + } +} diff --git a/extended/src/test/java/apoc/load/xls/LoadXlsTest.java b/extended/src/test/java/apoc/load/xls/LoadXlsTest.java index dec54f8aa4..7d40963dd4 100644 --- a/extended/src/test/java/apoc/load/xls/LoadXlsTest.java +++ b/extended/src/test/java/apoc/load/xls/LoadXlsTest.java @@ -10,6 +10,7 @@ import org.junit.Test; import org.junit.jupiter.api.AfterAll; +import org.neo4j.configuration.GraphDatabaseInternalSettings; import org.neo4j.graphdb.GraphDatabaseService; import org.neo4j.graphdb.QueryExecutionException; import org.neo4j.graphdb.Result; @@ -38,6 +39,8 @@ import static org.junit.Assert.*; public class LoadXlsTest { + + // TODO - roundtrip example with vectors... private static String loadTest = Thread.currentThread().getContextClassLoader().getResource("load_test.xlsx").getPath(); private static String testDate = Thread.currentThread().getContextClassLoader().getResource("test_date.xlsx").getPath(); @@ -45,7 +48,8 @@ public class LoadXlsTest { private static String testColumnsAfterZ = Thread.currentThread().getContextClassLoader().getResource("testLoadXlsColumnsAfterZ.xlsx").getPath(); @Rule - public DbmsRule db = new ImpermanentDbmsRule(); + public DbmsRule db = new ImpermanentDbmsRule() + .withSetting(GraphDatabaseInternalSettings.cypher_enable_vector_type, true); @Before public void setUp() throws Exception { TestUtil.registerProcedure(db, LoadXls.class);