From 7a21fea1d6e39c30896cf0b5ddf54defcc5264f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Louise=20S=C3=B6derstr=C3=B6m?= Date: Thu, 28 Sep 2017 11:38:02 +0200 Subject: [PATCH] Porting tests from NewPlannerTestSupport to CypherComparisonSupport. Move scala Notificaion tests from acceptance testsuite to java test file. Part of Cypher Comparison Support coverage --- .../java/org/neo4j/cypher/ChangedResults.java | 27 - .../NotificationAcceptanceTest.java | 805 ++++++++++++++---- .../NotificationAcceptanceTest.scala | 629 -------------- 3 files changed, 631 insertions(+), 830 deletions(-) delete mode 100644 community/cypher/cypher/src/test/java/org/neo4j/cypher/ChangedResults.java delete mode 100644 enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/NotificationAcceptanceTest.scala diff --git a/community/cypher/cypher/src/test/java/org/neo4j/cypher/ChangedResults.java b/community/cypher/cypher/src/test/java/org/neo4j/cypher/ChangedResults.java deleted file mode 100644 index 6d8cb7e804236..0000000000000 --- a/community/cypher/cypher/src/test/java/org/neo4j/cypher/ChangedResults.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2002-2017 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.neo4j.cypher; - -public class ChangedResults -{ - @Deprecated - public final String oldField = "deprecated"; - public final String newField = "use this"; -} diff --git a/community/cypher/cypher/src/test/java/org/neo4j/cypher/internal/javacompat/NotificationAcceptanceTest.java b/community/cypher/cypher/src/test/java/org/neo4j/cypher/internal/javacompat/NotificationAcceptanceTest.java index f3ca4a13b9cc8..209dc2f1736bb 100644 --- a/community/cypher/cypher/src/test/java/org/neo4j/cypher/internal/javacompat/NotificationAcceptanceTest.java +++ b/community/cypher/cypher/src/test/java/org/neo4j/cypher/internal/javacompat/NotificationAcceptanceTest.java @@ -26,7 +26,9 @@ import org.junit.Rule; import org.junit.Test; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Map; import java.util.stream.Stream; @@ -35,8 +37,12 @@ import org.neo4j.graphdb.Result; import org.neo4j.graphdb.SeverityLevel; import org.neo4j.graphdb.Transaction; +import org.neo4j.graphdb.impl.notification.NotificationCode; +import org.neo4j.graphdb.impl.notification.NotificationDetail; import org.neo4j.helpers.collection.Iterables; +import org.neo4j.kernel.impl.proc.Procedures; import org.neo4j.kernel.internal.GraphDatabaseAPI; +import org.neo4j.procedure.Procedure; import org.neo4j.test.rule.ImpermanentDatabaseRule; import static org.hamcrest.Matchers.any; @@ -44,11 +50,19 @@ import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertThat; +import static org.neo4j.graphdb.Label.label; import static org.neo4j.graphdb.impl.notification.NotificationCode.CREATE_UNIQUE_UNAVAILABLE_FALLBACK; +import static org.neo4j.graphdb.impl.notification.NotificationCode.EAGER_LOAD_CSV; +import static org.neo4j.graphdb.impl.notification.NotificationCode.INDEX_HINT_UNFULFILLABLE; +import static org.neo4j.graphdb.impl.notification.NotificationCode.LENGTH_ON_NON_PATH; import static org.neo4j.graphdb.impl.notification.NotificationCode.RULE_PLANNER_UNAVAILABLE_FALLBACK; +import static org.neo4j.graphdb.impl.notification.NotificationCode.RUNTIME_UNSUPPORTED; +import static org.neo4j.graphdb.impl.notification.NotificationCode.UNBOUNDED_SHORTEST_PATH; +import static org.neo4j.graphdb.impl.notification.NotificationDetail.Factory.index; public class NotificationAcceptanceTest { + @Rule public final ImpermanentDatabaseRule rule = new ImpermanentDatabaseRule(); @@ -60,7 +74,7 @@ public void shouldNotifyWhenUsingCypher3_1ForTheRulePlannerWhenCypherVersionIsTh InputPosition position = new InputPosition( 20, 1, 21 ); // then - assertThat( result.getNotifications(), Matchers.contains( RULE_PLANNER_UNAVAILABLE_FALLBACK.notification( position ) ) ); + assertThat( result.getNotifications(), Matchers.contains( RULE_PLANNER_UNAVAILABLE_FALLBACK.notification( position ) ) ); Map arguments = result.getExecutionPlanDescription().getArguments(); assertThat( arguments.get( "version" ), equalTo( "CYPHER 3.1" ) ); assertThat( arguments.get( "planner" ), equalTo( "RULE" ) ); @@ -75,7 +89,7 @@ public void shouldNotifyWhenUsingCypher3_1ForTheRulePlannerWhenCypherVersionIs3_ InputPosition position = new InputPosition( 24, 1, 25 ); // then - assertThat( result.getNotifications(), Matchers.contains( RULE_PLANNER_UNAVAILABLE_FALLBACK.notification( position ) ) ); + assertThat( result.getNotifications(), Matchers.contains( RULE_PLANNER_UNAVAILABLE_FALLBACK.notification( position ) ) ); Map arguments = result.getExecutionPlanDescription().getArguments(); assertThat( arguments.get( "version" ), equalTo( "CYPHER 3.1" ) ); assertThat( arguments.get( "planner" ), equalTo( "RULE" ) ); @@ -90,7 +104,7 @@ public void shouldNotifyWhenUsingCypher3_1ForTheRulePlannerWhenCypherVersionIs3_ InputPosition position = new InputPosition( 24, 1, 25 ); // then - assertThat( result.getNotifications(), Matchers.contains( RULE_PLANNER_UNAVAILABLE_FALLBACK.notification( position ) ) ); + assertThat( result.getNotifications(), Matchers.contains( RULE_PLANNER_UNAVAILABLE_FALLBACK.notification( position ) ) ); Map arguments = result.getExecutionPlanDescription().getArguments(); assertThat( arguments.get( "version" ), equalTo( "CYPHER 3.1" ) ); assertThat( arguments.get( "planner" ), equalTo( "RULE" ) ); @@ -114,6 +128,21 @@ public void shouldNotNotifyWhenUsingTheRulePlannerWhenCypherVersionIsNot3_2() th } ); } + @Test + public void shouldWarnWhenRequestingCompiledRuntimeOnUnsupportedQuery() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotifyInStream( version, "EXPLAIN CYPHER runtime=compiled MATCH (a)-->(b), (c)-->(d) RETURN count(*)", InputPosition.empty, + RUNTIME_UNSUPPORTED ) ); + } + + @Test + public void shouldWarnWhenRequestingSlottedRuntimeOnUnsupportedQuery() throws Exception + { + Stream.of( "CYPHER 3.3" ).forEach( + version -> shouldNotifyInStream( version, "explain cypher runtime=slotted merge (a)-[:X]->(b)", InputPosition.empty, RUNTIME_UNSUPPORTED ) ); + } + @Test public void shouldNotifyWhenUsingCreateUniqueWhenCypherVersionIsDefault() throws Exception { @@ -123,7 +152,7 @@ public void shouldNotifyWhenUsingCreateUniqueWhenCypherVersionIsDefault() throws // then assertThat( result.getNotifications(), - Matchers.contains( CREATE_UNIQUE_UNAVAILABLE_FALLBACK.notification( position ) ) ); + Matchers.contains( CREATE_UNIQUE_UNAVAILABLE_FALLBACK.notification( position ) ) ); Map arguments = result.getExecutionPlanDescription().getArguments(); assertThat( arguments.get( "version" ), equalTo( "CYPHER 3.1" ) ); result.close(); @@ -137,8 +166,7 @@ public void shouldNotifyWhenUsingCreateUniqueWhenCypherVersionIs3_3() throws Exc InputPosition position = new InputPosition( 36, 1, 37 ); // then - assertThat( result.getNotifications(), - Matchers.contains( CREATE_UNIQUE_UNAVAILABLE_FALLBACK.notification( position ) ) ); + assertThat( result.getNotifications(), Matchers.contains( CREATE_UNIQUE_UNAVAILABLE_FALLBACK.notification( position ) ) ); Map arguments = result.getExecutionPlanDescription().getArguments(); assertThat( arguments.get( "version" ), equalTo( "CYPHER 3.1" ) ); result.close(); @@ -152,8 +180,7 @@ public void shouldNotifyWhenUsingCreateUniqueWhenCypherVersionIs3_2() throws Exc InputPosition position = new InputPosition( 36, 1, 37 ); // then - assertThat( result.getNotifications(), - Matchers.contains( CREATE_UNIQUE_UNAVAILABLE_FALLBACK.notification( position ) ) ); + assertThat( result.getNotifications(), Matchers.contains( CREATE_UNIQUE_UNAVAILABLE_FALLBACK.notification( position ) ) ); Map arguments = result.getExecutionPlanDescription().getArguments(); assertThat( arguments.get( "version" ), equalTo( "CYPHER 3.1" ) ); result.close(); @@ -162,182 +189,517 @@ public void shouldNotifyWhenUsingCreateUniqueWhenCypherVersionIs3_2() throws Exc @Test public void shouldNotNotifyWhenUsingCreateUniqueWhenCypherVersionIsNot3_2() throws Exception { - Stream.of( "CYPHER 3.1", "CYPHER 2.3" ).forEach( version -> + Stream.of( "CYPHER 3.1", "CYPHER 2.3" ).forEach( + version -> shouldNotNotifyInStream( version, " MATCH (b) WITH b LIMIT 1 CREATE UNIQUE (b)-[:REL]->()" ) ); + } + + @Test + public void shouldWarnWhenUsingLengthOnNonPath() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + // pattern + shouldNotifyInStream( version, "explain match (a) where a.name='Alice' return length((a)-->()-->())", new InputPosition( 63, 1, 64 ), + LENGTH_ON_NON_PATH ); + + // collection + shouldNotifyInStream( version, " explain return length([1, 2, 3])", new InputPosition( 33, 1, 34 ), LENGTH_ON_NON_PATH ); + + // string + shouldNotifyInStream( version, " explain return length('a string')", new InputPosition( 33, 1, 34 ), LENGTH_ON_NON_PATH ); + } ); + } + + @Test + public void shouldNotNotifyWhenUsingLengthOnPath() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, " explain match p=(a)-[*]->(b) return length(p)" ) ); + } + + @Test + public void shouldNotNotifyWhenUsingSizeOnCollection() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, "explain return size([1, 2, 3])" ) ); + } + + @Test + public void shouldNotNotifyWhenUsingSizeOnString() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, " explain return size('a string')" ) ); + } + + @Test + public void shouldNotNotifyForCostUnsupportedUpdateQueryIfPlannerNotExplicitlyRequested() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, " EXPLAIN MATCH (n:Movie) SET n.title = 'The Movie'" ) ); + } + + @Test + public void shouldNotNotifyForCostSupportedUpdateQuery() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + shouldNotNotifyInStream( version, "EXPLAIN CYPHER planner=cost MATCH (n:Movie) SET n:Seen" ); + shouldNotNotifyInStream( version, "EXPLAIN CYPHER planner=idp MATCH (n:Movie) SET n:Seen" ); + shouldNotNotifyInStream( version, "EXPLAIN CYPHER planner=dp MATCH (n:Movie) SET n:Seen" ); + } ); + } + + @Test + public void shouldNotNotifyUsingJoinHintWithCost() throws Exception + { + List queries = Arrays.asList( "CYPHER planner=cost EXPLAIN MATCH (a)-->(b) USING JOIN ON b RETURN a, b", + "CYPHER planner=cost EXPLAIN MATCH (a)-->(x)<--(b) USING JOIN ON x RETURN a, b" ); + + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + for ( String query : queries ) + { + assertNotifications( version + query, containsNoItem( joinHintUnsuportedWarning ) ); + } + } ); + } + + @Test + public void shouldWarnOnPotentiallyCachedQueries() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + assertNotifications( version + "explain match (a)-->(b), (c)-->(d) return *", containsItem( cartesianProductWarning ) ); + + // no warning without explain + shouldNotNotifyInStream( version, "match (a)-->(b), (c)-->(d) return *" ); + } ); + } + + @Test + public void shouldWarnOnceWhenSingleIndexHintCannotBeFulfilled() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotifyInStreamWithDetail( version, " EXPLAIN MATCH (n:Person) USING INDEX n:Person(name) WHERE n.name = 'John' RETURN n", + InputPosition.empty, INDEX_HINT_UNFULFILLABLE, index( "Person", "name" ) ) ); + } + + @Test + public void shouldWarnOnEachUnfulfillableIndexHint() throws Exception + { + String query = " EXPLAIN MATCH (n:Person), (m:Party), (k:Animal) " + "USING INDEX n:Person(name) " + "USING INDEX m:Party(city) " + + "USING INDEX k:Animal(species) " + "WHERE n.name = 'John' AND m.city = 'Reykjavik' AND k.species = 'Sloth' " + "RETURN n"; + + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + shouldNotifyInStreamWithDetail( version, query, InputPosition.empty, INDEX_HINT_UNFULFILLABLE, index( "Person", "name" ) ); + shouldNotifyInStreamWithDetail( version, query, InputPosition.empty, INDEX_HINT_UNFULFILLABLE, index( "Party", "city" ) ); + shouldNotifyInStreamWithDetail( version, query, InputPosition.empty, INDEX_HINT_UNFULFILLABLE, index( "Animal", "species" ) ); + } ); + } + + @Test + public void shouldNotNotifyOnLiteralMaps() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> shouldNotNotifyInStream( version, " explain return { id: 42 } " ) ); + } + + @Test + public void shouldNotNotifyOnNonExistingLabelUsingLoadCSV() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + // create node + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE (n:Category)" ); + + // merge node + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row MERGE (n:Category)" ); + + // set label to node + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE (n) SET n:Category" ); + } ); + } + + @Test + public void shouldNotNotifyOnNonExistingRelTypeUsingLoadCSV() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + // create rel + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE ()-[:T]->()" ); + + // merge rel + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row MERGE ()-[:T]->()" ); + } ); + } + + @Test + public void shouldNotNotifyOnNonExistingPropKeyIdUsingLoadCSV() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + // create node + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE (n) SET n.p = 'a'" ); + + // merge node + shouldNotNotifyInStream( version, " EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row MERGE (n) ON CREATE SET n.p = 'a'" ); + } ); + } + + @Test + public void shouldNotNotifyOnEagerBeforeLoadCSV() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> shouldNotNotifyInStream( version, + "EXPLAIN MATCH (n) DELETE n WITH * LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MERGE () RETURN line" ) ); + } + + @Test + public void shouldWarnOnEagerAfterLoadCSV() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> shouldNotifyInStream( version, + "EXPLAIN MATCH (n) LOAD CSV FROM 'file:///ignore/ignore.csv' AS line WITH * DELETE n MERGE () RETURN line", InputPosition.empty, + EAGER_LOAD_CSV ) ); + } + + @Test + public void shouldNotNotifyOnLoadCSVWithoutEager() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, "EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MATCH (:A) CREATE (:B) RETURN line" ) ); + } + + @Test + public void shouldNotNotifyOnEagerWithoutLoadCSV() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> assertNotifications( version + "EXPLAIN MATCH (a), (b) CREATE (c) RETURN *", containsNoItem( eagerOperatorWarning ) ) ); + } + + @Test + public void shouldWarnOnLargeLabelScansWithLoadCVSMatch() throws Exception + { + for ( int i = 0; i < 11; i++ ) { - // when - Result result = db().execute( version + " MATCH (b) WITH b LIMIT 1 CREATE UNIQUE (b)-[:REL]->()" ); + try ( Transaction tx = db().beginTx() ) + { + db().createNode().addLabel( label( "A" ) ); + tx.success(); + } + } + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> assertNotifications( version + "EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MATCH (a:A) RETURN *", + containsNoItem( largeLabelCSVWarning ) ) ); + } - // then - assertThat( Iterables.asList( result.getNotifications() ), empty() ); - Map arguments = result.getExecutionPlanDescription().getArguments(); - assertThat( arguments.get( "version" ), equalTo( version ) ); - result.close(); + @Test + public void shouldWarnOnLargeLabelScansWithLoadCVSMerge() throws Exception + { + for ( int i = 0; i < 11; i++ ) + { + try ( Transaction tx = db().beginTx() ) + { + db().createNode().addLabel( label( "A" ) ); + tx.success(); + } + } + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> assertNotifications( version + "EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MERGE (a:A) RETURN *", + containsNoItem( largeLabelCSVWarning ) ) ); + } + + @Test + public void shouldNotWarnOnSmallLabelScansWithLoadCVS() throws Exception + { + try ( Transaction tx = db().beginTx() ) + { + db().createNode().addLabel( label( "A" ) ); + tx.success(); + } + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + shouldNotNotifyInStream( version, "EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MATCH (a:A) RETURN *" ); + shouldNotNotifyInStream( version, "EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MERGE (a:A) RETURN *" ); + } ); + } + + @Test + public void shouldWarnOnDeprecatedToInt() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> + assertNotifications( version + " EXPLAIN RETURN toInt('1') AS one", containsItem( deprecatedFeatureWarning ) ) ); + } + + @Test + public void shouldWarnOnDeprecatedUpper() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> + assertNotifications( version + " EXPLAIN RETURN upper('foo') AS one", containsItem( deprecatedFeatureWarning ) ) ); + } + + @Test + public void shouldWarnOnDeprecatedLower() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> + assertNotifications( version + " EXPLAIN RETURN lower('BAR') AS one", containsItem( deprecatedFeatureWarning ) ) ); + } + + @Test + public void shouldWarnOnDeprecatedRels() throws Exception + { + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> + assertNotifications( version + " EXPLAIN MATCH p = ()-->() RETURN rels(p) AS r", containsItem( deprecatedFeatureWarning ) ) ); + } + + @Test + public void shouldWarnOnDeprecatedProcedureCalls() throws Exception + { + db().getDependencyResolver().provideDependency( Procedures.class ).get().registerProcedure( TestProcedures.class ); + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + assertNotifications( version + "explain CALL oldProc()", containsItem( deprecatedProcedureWarning ) ); + assertNotifications( version + "explain CALL oldProc() RETURN 1", containsItem( deprecatedProcedureWarning ) ); + } ); + } + + @Test + public void shouldWarnOnDeprecatedProcedureResultField() throws Exception + { + db().getDependencyResolver().provideDependency( Procedures.class ).get().registerProcedure( TestProcedures.class ); + Stream.of( "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> assertNotifications( version + "explain CALL changedProc() YIELD oldField RETURN oldField", + containsItem( deprecatedProcedureReturnFieldWarning ) ) ); + } + + @Test + public void shouldWarnOnUnboundedShortestPath() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotifyInStream( version, "EXPLAIN MATCH p = shortestPath((n)-[*]->(m)) RETURN m", new InputPosition( 44, 1, 45 ), + UNBOUNDED_SHORTEST_PATH ) ); + } + + @Test + public void shouldNotNotifyOnDynamicPropertyLookupWithNoLabels() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + db().execute( "CREATE INDEX ON :Person(name)" ); + db().execute( "Call db.awaitIndexes()" ); + shouldNotNotifyInStream( version, "EXPLAIN MATCH (n) WHERE n['key-' + n.name] = 'value' RETURN n" ); + } ); + } + + @Test + public void shouldWarnOnDynamicPropertyLookupWithBothStaticAndDynamicProperties() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + db().execute( "CREATE INDEX ON :Person(name)" ); + db().execute( "Call db.awaitIndexes()" ); + shouldNotNotifyInStream( version, "EXPLAIN MATCH (n) WHERE n['key-' + n.name] = 'value' RETURN n" ); + } ); + } + + @Test + public void shouldNotNotifyOnDynamicPropertyLookupWithLabelHavingNoIndex() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + db().execute( "CREATE INDEX ON :Person(name)" ); + db().execute( "Call db.awaitIndexes()" ); + try ( Transaction tx = db().beginTx() ) + { + db().createNode().addLabel( label( "Foo" ) ); + tx.success(); + } + shouldNotNotifyInStream( version, "EXPLAIN MATCH (n) WHERE n['key-' + n.name] = 'value' RETURN n" ); + } ); + } + + @Test + public void shouldWarnOnUnfulfillableIndexSeekUsingDynamicProperty() throws Exception + { + List queries = new ArrayList<>(); + + // dynamic property lookup with single label + queries.add( "EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] = 'value' RETURN n" ); + + // dynamic property lookup with explicit label check + queries.add( "EXPLAIN MATCH (n) WHERE n['key-' + n.name] = 'value' AND (n:Person) RETURN n" ); + + // dynamic property lookup with range seek + queries.add( "EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] > 10 RETURN n" ); + + // dynamic property lookup with range seek (reverse) + queries.add( "EXPLAIN MATCH (n:Person) WHERE 10 > n['key-' + n.name] RETURN n" ); + + // dynamic property lookup with a single label and property existence check with exists + queries.add( "EXPLAIN MATCH (n:Person) WHERE exists(n['na' + 'me']) RETURN n" ); + + // dynamic property lookup with a single label and starts with + queries.add( "EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] STARTS WITH 'Foo' RETURN n" ); + + // dynamic property lookup with a single label and regex + queries.add( "EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] =~ 'Foo*' RETURN n" ); + + // dynamic property lookup with a single label and IN + queries.add( "EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] IN ['Foo', 'Bar'] RETURN n" ); + + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + for ( String query : queries ) + { + db().execute( "CREATE INDEX ON :Person(name)" ); + db().execute( "Call db.awaitIndexes()" ); + assertNotifications( version + query, containsItem( dynamicPropertyWarning ) ); + } + } ); + } + + @Test + public void shouldNotNotifyOnDynamicPropertyLookupWithSingleLabelAndNegativePredicate() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + db().execute( "CREATE INDEX ON :Person(name)" ); + db().execute( "Call db.awaitIndexes()" ); + shouldNotNotifyInStream( version, "EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] <> 'value' RETURN n" ); + } ); + } + + @Test + public void shouldWarnOnUnfulfillableIndexSeekUsingDynamicPropertyAndMultipleLabels() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + + db().execute( "CREATE INDEX ON :Person(name)" ); + db().execute( "CREATE INDEX ON :Jedi(weapon)" ); + db().execute( "Call db.awaitIndexes()" ); + + assertNotifications( version + "EXPLAIN MATCH (n:Person:Jedi) WHERE n['key-' + n.name] = 'value' RETURN n", + containsItem( dynamicPropertyWarning ) ); } ); } @Test public void shouldWarnOnFutureAmbiguousRelTypeSeparator() throws Exception { - for ( String pattern : Arrays.asList( "[:A|:B|:C {foo:'bar'}]", "[:A|:B|:C*]", "[x:A|:B|:C]" ) ) + List deprecatedQueries = Arrays.asList( "explain MATCH (a)-[:A|:B|:C {foo:'bar'}]-(b) RETURN a,b", "explain MATCH (a)-[x:A|:B|:C]-() RETURN a", + "explain MATCH (a)-[:A|:B|:C*]-() RETURN a" ); + + List nonDeprecatedQueries = + Arrays.asList( "explain MATCH (a)-[:A|B|C {foo:'bar'}]-(b) RETURN a,b", "explain MATCH (a)-[:A|:B|:C]-(b) RETURN a,b", + "explain MATCH (a)-[:A|B|C]-(b) RETURN a,b" ); + + for ( String query : deprecatedQueries ) { - assertNotifications( "CYPHER 3.3 explain MATCH (a)-" + pattern + "-(b) RETURN a,b", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "The semantics of using colon in the separation of alternative relationship " + - "types in conjunction with the use of variable binding, inlined property " + - "predicates, or variable length will change in a future version." - ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "CYPHER 3.3 " + query, containsItem( deprecatedSeparatorWarning ) ); + } + + for ( String query : nonDeprecatedQueries ) + { + assertNotifications( "CYPHER 3.3 " + query, containsNoItem( deprecatedSeparatorWarning ) ); } } @Test public void shouldWarnOnBindingVariableLengthRelationship() throws Exception { - assertNotifications( "CYPHER 3.3 explain MATCH ()-[rs*]-() RETURN rs", containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( "Binding relationships to a list in a variable length pattern is deprecated." ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "CYPHER 3.3 explain MATCH ()-[rs*]-() RETURN rs", containsItem( depracatedBindingWarning ) ); + + assertNotifications( "CYPHER 3.3 explain MATCH p = ()-[*]-() RETURN relationships(p) AS rs", containsNoItem( depracatedBindingWarning ) ); + } + + @Test + public void shouldWarnOnCartesianProduct() throws Exception + { + + Stream.of( "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( version -> { + assertNotifications( version + "explain match (a)-->(b), (c)-->(d) return *", containsItem( cartesianProductWarning ) ); + + assertNotifications( version + "explain cypher runtime=compiled match (a)-->(b), (c)-->(d) return *", containsItem( cartesianProductWarning ) ); + + assertNotifications( version + "explain cypher runtime=interpreted match (a)-->(b), (c)-->(d) return *", containsItem( cartesianProductWarning ) ); + } ); + } + + @Test + public void shouldNotNotifyOnCartesianProductWithoutExplain() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, " match (a)-->(b), (c)-->(d) return *" ) ); } @Test public void shouldWarnOnMissingLabel() throws Exception { - assertNotifications( "EXPLAIN MATCH (a:NO_SUCH_THING) RETURN a", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: NO_SUCH_THING)" ), - equalTo( new InputPosition( 17, 1, 18 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN MATCH (a:NO_SUCH_THING) RETURN a", containsItem( unknownLabelWarning ) ); } @Test public void shouldWarnOnMissingLabelWithCommentInBeginningWithOlderCypherVersions() throws Exception { - assertNotifications( "CYPHER 2.3 EXPLAIN//TESTING \nMATCH (n:X) return n Limit 1", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: X)" ), - equalTo( new InputPosition( 38, 2, 10 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "CYPHER 2.3 EXPLAIN//TESTING \nMATCH (n:X) return n Limit 1", containsItem( unknownLabelWarning ) ); - assertNotifications( "CYPHER 3.1 EXPLAIN//TESTING \nMATCH (n:X) return n Limit 1", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: X)" ), - equalTo( new InputPosition( 38, 2, 10 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "CYPHER 3.1 EXPLAIN//TESTING \nMATCH (n:X) return n Limit 1", containsItem( unknownLabelWarning ) ); } @Test public void shouldWarnOnMissingLabelWithCommentInBeginning() throws Exception { - assertNotifications( "EXPLAIN//TESTING \nMATCH (n:X) return n Limit 1", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: X)" ), - equalTo( new InputPosition( 27, 2, 10 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN//TESTING \nMATCH (n:X) return n Limit 1", containsItem( unknownLabelWarning ) ); } @Test public void shouldWarnOnMissingLabelWithCommentInBeginningTwoLines() throws Exception { - assertNotifications( "//TESTING \n //TESTING \n EXPLAIN MATCH (n)\n MATCH (b:X) return n,b Limit 1", - containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: X)" ), - equalTo( new InputPosition( 52, 4, 11 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "//TESTING \n //TESTING \n EXPLAIN MATCH (n)\n MATCH (b:X) return n,b Limit 1", containsItem( unknownLabelWarning ) ); } @Test public void shouldWarnOnMissingLabelWithCommentInBeginningOnOneLine() throws Exception { - assertNotifications( "explain /* Testing */ MATCH (n:X) RETURN n", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: X)" ), - equalTo( new InputPosition( 31, 1, 32 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "explain /* Testing */ MATCH (n:X) RETURN n", containsItem( unknownLabelWarning ) ); } @Test public void shouldWarnOnMissingLabelWithCommentInMiddel() throws Exception { - assertNotifications( "EXPLAIN\nMATCH (n)\n//TESTING \nMATCH (n:X)\nreturn n Limit 1", - containsItem( notification( - "Neo.ClientNotification.Statement.UnknownLabelWarning", - containsString( "the missing label name is: X)" ), - equalTo( new InputPosition( 38, 4, 10 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN\nMATCH (n)\n//TESTING \nMATCH (n:X)\nreturn n Limit 1", containsItem( unknownLabelWarning ) ); + } + + @Test + public void shouldNotNotifyForMissingLabelOnUpdate() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, " EXPLAIN CREATE (n:Person)" ) ); } @Test public void shouldWarnOnMissingRelationshipType() throws Exception { - assertNotifications( "EXPLAIN MATCH ()-[a:NO_SUCH_THING]->() RETURN a", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning", - containsString( "the missing relationship type is: NO_SUCH_THING)" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN MATCH ()-[a:NO_SUCH_THING]->() RETURN a", containsItem( unknownRelatonshipWarning ) ); } @Test public void shouldWarnOnMissingRelationshipTypeWithComment() throws Exception { - assertNotifications( "EXPLAIN /*Comment*/ MATCH ()-[a:NO_SUCH_THING]->() RETURN a", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning", - containsString( "the missing relationship type is: NO_SUCH_THING)" ), - equalTo( new InputPosition( 32, 1, 33 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN /*Comment*/ MATCH ()-[a:NO_SUCH_THING]->() RETURN a", containsItem( unknownRelatonshipWarning ) ); } @Test public void shouldWarnOnMissingProperty() throws Exception { - assertNotifications( "EXPLAIN MATCH (a {NO_SUCH_THING: 1337}) RETURN a", containsItem( notification( - "Neo.ClientNotification.Statement.UnknownPropertyKeyWarning", - containsString( "the missing property name is: NO_SUCH_THING)" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN MATCH (a {NO_SUCH_THING: 1337}) RETURN a", containsItem( unknownPropertyKeyWarning ) ); + } + + @Test + public void shouldNotNotifyForMissingPropertiesOnUpdate() throws Exception + { + Stream.of( "CYPHER 2.3", "CYPHER 3.1", "CYPHER 3.2", "CYPHER 3.3" ).forEach( + version -> shouldNotNotifyInStream( version, " EXPLAIN CREATE (n {prop: 42})" ) ); } @Test public void shouldWarnThatStartIsDeprecatedForAllNodeScan() { - assertNotifications( "EXPLAIN START n=node(*) RETURN n", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "START has been deprecated and will be removed in a future version. (START is " + - "deprecated, use: `MATCH (n)`" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START n=node(*) RETURN n", containsItem( deprecatedStartWarning ) ); } @Test public void shouldWarnThatStartIsDeprecatedForNodeById() { - assertNotifications( "EXPLAIN START n=node(1337) RETURN n", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "START has been deprecated and will be removed in a future version. (START is " + - "deprecated, use: `MATCH (n) WHERE id(n) = 1337`" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START n=node(1337) RETURN n", containsItem( deprecatedStartWarning ) ); } @Test public void shouldWarnThatStartIsDeprecatedForNodeByIds() { - assertNotifications( "EXPLAIN START n=node(42,1337) RETURN n", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "START has been deprecated and will be removed in a future version. (START is " + - "deprecated, use: `MATCH (n) WHERE id(n) IN [42, 1337]`" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START n=node(42,1337) RETURN n", containsItem( deprecatedStartWarning ) ); } @Test @@ -347,15 +709,7 @@ public void shouldWarnThatStartIsDeprecatedForNodeIndexSeek() { db().index().forNodes( "index" ); } - assertNotifications( "EXPLAIN START n=node:index(key = 'value') RETURN n", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( "START has been deprecated and will be removed in a future version. " + - "(START is deprecated, use: " + - "`CALL db.index.explicit.seekNodes('index', 'key', 'value') YIELD node AS n` " + - "instead." ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START n=node:index(key = 'value') RETURN n", containsItem( deprecatedStartWarning ) ); } @Test @@ -365,54 +719,25 @@ public void shouldWarnThatStartIsDeprecatedForNodeIndexSearch() { db().index().forNodes( "index" ); } - assertNotifications( "EXPLAIN START n=node:index('key:value*') RETURN n", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( "START has been deprecated and will be removed in a future version. " + - "(START is deprecated, use: " + - "`CALL db.index.explicit.searchNodes('index', 'key:value*') YIELD node AS n` " + - "instead." ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START n=node:index('key:value*') RETURN n", containsItem( deprecatedStartWarning ) ); } @Test public void shouldWarnThatStartIsDeprecatedForAllRelScan() { - assertNotifications( "EXPLAIN START r=relationship(*) RETURN r", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "START has been deprecated and will be removed in a future version. (START is " + - "deprecated, use: `MATCH ()-[r]->()`" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START r=relationship(*) RETURN r", containsItem( deprecatedStartWarning ) ); } @Test public void shouldWarnThatStartIsDeprecatedForRelById() { - assertNotifications( "EXPLAIN START r=relationship(1337) RETURN r", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "START has been deprecated and will be removed in a future version. (START is " + - "deprecated, use: `MATCH ()-[r]->() WHERE id(r) = 1337`" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START r=relationship(1337) RETURN r", containsItem( deprecatedStartWarning ) ); } @Test public void shouldWarnThatStartIsDeprecatedForRelByIds() { - assertNotifications( "EXPLAIN START r=relationship(42,1337) RETURN r", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( - "START has been deprecated and will be removed in a future version. (START is " + - "deprecated, use: `MATCH ()-[r]->() WHERE id(r) IN [42, 1337]`" ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START r=relationship(42,1337) RETURN r", containsItem( deprecatedStartWarning ) ); } @Test @@ -422,15 +747,7 @@ public void shouldWarnThatStartIsDeprecatedForRelIndexSeek() { db().index().forRelationships( "index" ); } - assertNotifications( "EXPLAIN START r=relationship:index(key = 'value') RETURN r", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( "START has been deprecated and will be removed in a future version. " + - "(START is deprecated, use: " + - "`CALL db.index.explicit.seekRelationships('index', 'key', 'value') YIELD " + - "relationship AS r` instead." ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START r=relationship:index(key = 'value') RETURN r", containsItem( deprecatedStartWarning ) ); } @Test @@ -440,26 +757,13 @@ public void shouldWarnThatStartIsDeprecatedForRelIndexSearch() { db().index().forRelationships( "index" ); } - assertNotifications( "EXPLAIN START r=relationship:index('key:value*') RETURN r", - containsItem( notification( - "Neo.ClientNotification.Statement.FeatureDeprecationWarning", - containsString( "START has been deprecated and will be removed in a future version. " + - "(START is deprecated, use: " + - "`CALL db.index.explicit.searchRelationships('index', 'key:value*') YIELD " + - "relationship AS r` instead." ), - any( InputPosition.class ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN START r=relationship:index('key:value*') RETURN r", containsItem( deprecatedStartWarning ) ); } @Test public void shouldWarnOnMissingPropertyWithComment() throws Exception { - assertNotifications( "EXPLAIN /*Comment*/ MATCH (a {NO_SUCH_THING: 1337}) RETURN a", - containsItem( - notification( "Neo.ClientNotification.Statement.UnknownPropertyKeyWarning", - containsString( "the missing property name is: NO_SUCH_THING)" ), - equalTo( new InputPosition( 30, 1, 31 ) ), - SeverityLevel.WARNING ) ) ); + assertNotifications( "EXPLAIN /*Comment*/ MATCH (a {NO_SUCH_THING: 1337}) RETURN a", containsItem( unknownPropertyKeyWarning ) ); } private void assertNotifications( String query, Matcher> matchesExpectation ) @@ -528,4 +832,157 @@ public void describeTo( Description description ) } }; } + + private Matcher> containsNoItem( Matcher itemMatcher ) + { + return new TypeSafeMatcher>() + { + @Override + protected boolean matchesSafely( Iterable items ) + { + for ( T item : items ) + { + if ( itemMatcher.matches( item ) ) + { + return false; + } + } + return true; + } + + @Override + public void describeTo( Description description ) + { + description.appendText( "an iterable not containing " ).appendDescriptionOf( itemMatcher ); + } + }; + } + + private void shouldNotifyInStream( String version, String query, InputPosition pos, NotificationCode code ) + { + //when + Result result = db().execute( version + query ); + + //then + NotificationCode.Notification notification = code.notification( pos ); + assertThat( Iterables.asList( result.getNotifications() ), Matchers.hasItems( notification ) ); + Map arguments = result.getExecutionPlanDescription().getArguments(); + assertThat( arguments.get( "version" ), equalTo( version ) ); + result.close(); + } + + private void shouldNotifyInStreamWithDetail( String version, String query, InputPosition pos, NotificationCode code, NotificationDetail detail ) + { + //when + Result result = db().execute( version + query ); + + //then + NotificationCode.Notification notification = code.notification( pos, detail ); + assertThat( Iterables.asList( result.getNotifications() ), Matchers.hasItems( notification ) ); + Map arguments = result.getExecutionPlanDescription().getArguments(); + assertThat( arguments.get( "version" ), equalTo( version ) ); + result.close(); + } + + private void shouldNotNotifyInStream( String version, String query ) + { + // when + Result result = db().execute( version + query ); + + // then + assertThat( Iterables.asList( result.getNotifications() ), empty() ); + Map arguments = result.getExecutionPlanDescription().getArguments(); + assertThat( arguments.get( "version" ), equalTo( version ) ); + result.close(); + } + + public static class ChangedResults + { + @Deprecated + public final String oldField = "deprecated"; + public final String newField = "use this"; + } + + public static class TestProcedures + { + + @Procedure( "newProc" ) + public void newProc() + { + } + + @Deprecated + @Procedure( name = "oldProc", deprecatedBy = "newProc" ) + public void oldProc() + { + } + + @Procedure( "changedProc" ) + public Stream changedProc() + { + return Stream.of( new ChangedResults() ); + } + } + + private Matcher cartesianProductWarning = notification( "Neo.ClientNotification.Statement.CartesianProductWarning", containsString( + "If a part of a query contains multiple disconnected patterns, this will build a " + + "cartesian product between all those parts. This may produce a large amount of data and slow down" + " query processing. " + + "While occasionally intended, it may often be possible to reformulate the query that avoids the " + "use of this cross " + + "product, perhaps by adding a relationship between the different parts or by using OPTIONAL MATCH" ), any( InputPosition.class ), + SeverityLevel.WARNING ); + + private Matcher largeLabelCSVWarning = notification( "Neo.ClientNotification.Statement.NoApplicableIndexWarning", containsString( + "Using LOAD CSV with a large data set in a query where the execution plan contains the " + + "Using LOAD CSV followed by a MATCH or MERGE that matches a non-indexed label will most likely " + + "not perform well on large data sets. Please consider using a schema index." ), any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher deprecatedFeatureWarning = + notification( "Neo.ClientNotification.Statement.FeatureDeprecationWarning", containsString( "The query used a deprecated function." ), + any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher deprecatedStartWarning = notification( "Neo.ClientNotification.Statement.FeatureDeprecationWarning", + containsString( "START has been deprecated and will be removed in a future version. " ), any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher deprecatedProcedureWarning = + notification( "Neo.ClientNotification.Statement.FeatureDeprecationWarning", containsString( "The query used a deprecated procedure." ), + any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher deprecatedProcedureReturnFieldWarning = + notification( "Neo.ClientNotification.Statement.FeatureDeprecationWarning", containsString( "The query used a deprecated field from a procedure." ), + any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher depracatedBindingWarning = notification( "Neo.ClientNotification.Statement.FeatureDeprecationWarning", + containsString( "Binding relationships to a list in a variable length pattern is deprecated." ), any( InputPosition.class ), + SeverityLevel.WARNING ); + + private Matcher deprecatedSeparatorWarning = notification( "Neo.ClientNotification.Statement.FeatureDeprecationWarning", containsString( + "The semantics of using colon in the separation of alternative relationship " + + "types in conjunction with the use of variable binding, inlined property " + + "predicates, or variable length will change in a future version." ), any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher eagerOperatorWarning = notification( "Neo.ClientNotification.Statement.EagerOperatorWarning", containsString( + "Using LOAD CSV with a large data set in a query where the execution plan contains the " + + "Eager operator could potentially consume a lot of memory and is likely to not perform well. " + + "See the Neo4j Manual entry on the Eager operator for more information and hints on " + "how problems could be avoided." ), + any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher unknownPropertyKeyWarning = + notification( "Neo.ClientNotification.Statement.UnknownPropertyKeyWarning", containsString( "the missing property name is" ), + any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher unknownRelatonshipWarning = + notification( "Neo.ClientNotification.Statement.UnknownRelationshipTypeWarning", containsString( "the missing relationship type is" ), + any( InputPosition.class ), SeverityLevel.WARNING ); + + private Matcher unknownLabelWarning = + notification( "Neo.ClientNotification.Statement.UnknownLabelWarning", containsString( "the missing label name is" ), any( InputPosition.class ), + SeverityLevel.WARNING ); + + private Matcher dynamicPropertyWarning = notification( "Neo.ClientNotification.Statement.DynamicPropertyWarning", + containsString( "Using a dynamic property makes it impossible to use an index lookup for this query" ), any( InputPosition.class ), + SeverityLevel.WARNING ); + + private Matcher joinHintUnsuportedWarning = notification( "Neo.Status.Statement.JoinHintUnsupportedWarning", + containsString( "Using RULE planner is unsupported for queries with join hints, please use COST planner instead" ), any( InputPosition.class ), + SeverityLevel.WARNING ); } diff --git a/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/NotificationAcceptanceTest.scala b/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/NotificationAcceptanceTest.scala deleted file mode 100644 index 598a0c2286050..0000000000000 --- a/enterprise/cypher/acceptance-spec-suite/src/test/scala/org/neo4j/internal/cypher/acceptance/NotificationAcceptanceTest.scala +++ /dev/null @@ -1,629 +0,0 @@ -/* - * Copyright (c) 2002-2017 "Neo Technology," - * Network Engine for Objects in Lund AB [http://neotechnology.com] - * - * This file is part of Neo4j. - * - * Neo4j is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package org.neo4j.internal.cypher.acceptance - -import org.neo4j.cypher.internal.frontend.v3_3.notification._ -import org.neo4j.cypher.{ChangedResults, ExecutionEngineFunSuite, NewPlannerTestSupport} -import org.neo4j.graphdb -import org.neo4j.graphdb.impl.notification.NotificationCode._ -import org.neo4j.graphdb.impl.notification.NotificationDetail.Factory._ -import org.neo4j.graphdb.impl.notification.{NotificationCode, NotificationDetail} -import org.neo4j.kernel.impl.proc.Procedures -import org.neo4j.procedure.Procedure - -import scala.collection.JavaConverters._ - -class NotificationAcceptanceTest extends ExecutionEngineFunSuite with NewPlannerTestSupport { - - override def initTest(): Unit = { - super.initTest() - val procedures = this.graph.getDependencyResolver.resolveDependency(classOf[Procedures]) - procedures.registerProcedure(classOf[NotificationAcceptanceTest.TestProcedures]) - } - - test("Warn on future ambiguous separator between alternative relationship types") { - val res1 = innerExecute("explain MATCH (a)-[:A|:B|:C {foo:'bar'}]-(b) RETURN a,b") - - res1.notifications should contain( - DEPRECATED_RELATIONSHIP_TYPE_SEPARATOR.notification(new graphdb.InputPosition(17, 1, 18))) - - val res2 = innerExecute("explain MATCH (a)-[:A|B|C {foo:'bar'}]-(b) RETURN a,b") - - res2.notifications.map(_.getTitle) should not contain "Neo.ClientNotification.Statement.FeatureDeprecationWarning." - - val res3 = innerExecute("explain MATCH (a)-[:A|:B|:C]-(b) RETURN a,b") - - res3.notifications.map(_.getTitle) should not contain "Neo.ClientNotification.Statement.FeatureDeprecationWarning." - - val res4 = innerExecute("explain MATCH (a)-[:A|B|C]-(b) RETURN a,b") - - res4.notifications.map(_.getTitle) should not contain "Neo.ClientNotification.Statement.FeatureDeprecationWarning." - - val res5 = innerExecute("explain MATCH (a)-[x:A|:B|:C]-() RETURN a") - - res5.notifications should contain( - DEPRECATED_RELATIONSHIP_TYPE_SEPARATOR.notification(new graphdb.InputPosition(17, 1, 18))) - - val res6 = innerExecute("explain MATCH (a)-[:A|:B|:C*]-() RETURN a") - - res6.notifications should contain( - DEPRECATED_RELATIONSHIP_TYPE_SEPARATOR.notification(new graphdb.InputPosition(17, 1, 18))) - } - - test("Warn on binding variable length relationships") { - val res1 = innerExecute("explain MATCH ()-[rs*]-() RETURN rs") - - res1.notifications should contain( - DEPRECATED_BINDING_VAR_LENGTH_RELATIONSHIP.notification(new graphdb.InputPosition(16, 1, 17), - bindingVarLengthRelationship("rs"))) - - val res2 = innerExecute("explain MATCH p = ()-[*]-() RETURN relationships(p) AS rs") - - res2.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.FeatureDeprecationWarning." - } - - test("Warn on deprecated standalone procedure calls") { - val result = innerExecute("explain CALL oldProc()") - - result.notifications.toList should equal( - List( - DEPRECATED_PROCEDURE.notification(new graphdb.InputPosition(8, 1, 9), deprecatedName("oldProc", "newProc")))) - } - - test("Warn on deprecated in-query procedure calls") { - val result = innerExecute("explain CALL oldProc() RETURN 1") - - result.notifications.toList should equal( - List(DEPRECATED_PROCEDURE.notification(new graphdb.InputPosition(8, 1, 9), deprecatedName("oldProc", "newProc")))) - } - - test("Warn on deprecated procedure result field") { - val result = innerExecute("explain CALL changedProc() YIELD oldField RETURN oldField") - - result.notifications.toList should equal( - List( - DEPRECATED_PROCEDURE_RETURN_FIELD.notification(new graphdb.InputPosition(33, 1, 34), - deprecatedField("changedProc", "oldField")))) - } - - test("Warn for cartesian product") { - val result = executeWithAllPlannersAndRuntimesAndCompatibilityMode("explain match (a)-->(b), (c)-->(d) return *") - - result.notifications.toList should equal(List( - CARTESIAN_PRODUCT.notification(new graphdb.InputPosition(32, 1, 33), cartesianProduct(Set("c", "d").asJava)))) - } - - test("Warn for cartesian product with runtime=compiled") { - val result = innerExecute("explain cypher runtime=compiled match (a)-->(b), (c)-->(d) return count(*)") - - result.notifications.toList should equal(List( - CARTESIAN_PRODUCT.notification(new graphdb.InputPosition(32, 1, 33), cartesianProduct(Set("c", "d").asJava)), - RUNTIME_UNSUPPORTED.notification(graphdb.InputPosition.empty))) - } - - test("Warn unsupported runtime with explain and runtime=slotted") { - val result = innerExecute("explain cypher runtime=slotted merge (a)-[:X]->(b)") - - result.notifications.toList should equal(List( - RUNTIME_UNSUPPORTED.notification(graphdb.InputPosition.empty))) - } - - test("Warn for cartesian product with runtime=interpreted") { - val result = innerExecute("explain cypher runtime=interpreted match (a)-->(b), (c)-->(d) return *") - - result.notifications.toList should equal(List( - CARTESIAN_PRODUCT.notification(new graphdb.InputPosition(35, 1, 36), cartesianProduct(Set("c", "d").asJava)))) - } - - test("Don't warn for cartesian product when not using explain") { - val result = executeWithAllPlannersAndRuntimesAndCompatibilityMode("match (a)-->(b), (c)-->(d) return *") - - result.notifications shouldBe empty - } - - test("warn when using length on collection") { - val result = innerExecute("explain return length([1, 2, 3])") - - result.notifications should equal(Set( - LENGTH_ON_NON_PATH.notification(new graphdb.InputPosition(22, 1, 23)))) - } - - test("do not warn when using length on a path") { - val result = innerExecute("explain match p=(a)-[*]->(b) return length(p)") - - result.notifications shouldBe empty - } - - test("do warn when using length on a pattern expression") { - val result = executeWithAllPlannersAndCompatibilityMode( - "explain match (a) where a.name='Alice' return length((a)-->()-->())") - - result.notifications should contain(LENGTH_ON_NON_PATH.notification(new graphdb.InputPosition(77, 1, 78))) - } - - test("do warn when using length on a string") { - val result = innerExecute("explain return length('a string')") - - result.notifications should equal(Set(LENGTH_ON_NON_PATH.notification(new graphdb.InputPosition(22, 1, 23)))) - } - - test("do not warn when using size on a collection") { - val result = innerExecute("explain return size([1, 2, 3])") - result.notifications shouldBe empty - } - - test("do not warn when using size on a string") { - val result = innerExecute("explain return size('a string')") - result.notifications shouldBe empty - } - - test("do not warn for cost unsupported on update query if planner not explicitly requested") { - val result = innerExecute("EXPLAIN MATCH (n:Movie) SET n.title = 'The Movie'") - result.notifications should not contain PlannerUnsupportedNotification - } - - test("do not warn for cost unsupported when requesting COST on a supported update query") { - val result = innerExecute("EXPLAIN CYPHER planner=cost MATCH (n:Movie) SET n:Seen") - result.notifications should not contain PlannerUnsupportedNotification - } - - test("do not warn for cost unsupported when requesting IDP on a supported update query") { - val result = innerExecute("EXPLAIN CYPHER planner=idp MATCH (n:Movie) SET n:Seen") - result.notifications should not contain PlannerUnsupportedNotification - } - - test("do not warn for cost unsupported when requesting DP on a supported update query") { - val result = innerExecute("EXPLAIN CYPHER planner=dp MATCH (n:Movie) SET n:Seen") - result.notifications should not contain PlannerUnsupportedNotification - } - - test("warn when requesting runtime=compiled on an unsupported query") { - val result = innerExecute("EXPLAIN CYPHER runtime=compiled MATCH (a)-->(b), (c)-->(d) RETURN count(*)") - result.notifications should contain(RUNTIME_UNSUPPORTED.notification(graphdb.InputPosition.empty)) - } - - test("warn once when a single index hint cannot be fulfilled") { - val result = innerExecute("EXPLAIN MATCH (n:Person) USING INDEX n:Person(name) WHERE n.name = 'John' RETURN n") - result.notifications.toSet should contain( - INDEX_HINT_UNFULFILLABLE.notification(graphdb.InputPosition.empty, index("Person", "name"))) - } - - test("warn for each unfulfillable index hint") { - val result = innerExecute( - """EXPLAIN MATCH (n:Person), (m:Party), (k:Animal) - |USING INDEX n:Person(name) - |USING INDEX m:Party(city) - |USING INDEX k:Animal(species) - |WHERE n.name = 'John' AND m.city = 'Reykjavik' AND k.species = 'Sloth' - |RETURN n""".stripMargin) - - result.notifications should contain( - INDEX_HINT_UNFULFILLABLE.notification(graphdb.InputPosition.empty, index("Person", "name"))) - result.notifications should contain( - INDEX_HINT_UNFULFILLABLE.notification(graphdb.InputPosition.empty, index("Party", "city"))) - result.notifications should contain( - INDEX_HINT_UNFULFILLABLE.notification(graphdb.InputPosition.empty, index("Animal", "species"))) - } - - test("should not warn when join hint is used with COST planner") { - val result = innerExecute( """CYPHER planner=cost EXPLAIN MATCH (a)-->(b) USING JOIN ON b RETURN a, b""") - - result.notifications should not contain "Neo.Status.Statement.JoinHintUnsupportedWarning" - } - - test("should not warn when join hint is used with COST planner with EXPLAIN") { - val result = innerExecute( """CYPHER planner=cost EXPLAIN MATCH (a)-->(x)<--(b) USING JOIN ON x RETURN a, b""") - - result.notifications.map(_.getCode) should not contain "Neo.Status.Statement.JoinHintUnsupportedWarning" - } - - test("Warnings should work on potentially cached queries") { - val resultWithoutExplain = executeWithAllPlannersAndRuntimesAndCompatibilityMode( - "match (a)-->(b), (c)-->(d) return *") - val resultWithExplain = executeWithAllPlannersAndRuntimesAndCompatibilityMode( - "explain match (a)-->(b), (c)-->(d) return *") - - resultWithoutExplain shouldBe empty - resultWithExplain.notifications.toList should equal( - List(CARTESIAN_PRODUCT.notification(new graphdb.InputPosition(24, 1, 25), cartesianProduct(Set("c", "d").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with a single label") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] = 'value' RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with explicit label check") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n) WHERE n['key-' + n.name] = 'value' AND (n:Person) RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)) - )) - } - - test( - "warn for unfulfillable index seek when using dynamic property lookup with a single label and negative predicate") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] <> 'value' RETURN n") - - result.notifications shouldBe empty - } - - test("warn for unfulfillable index seek when using dynamic property lookup with range seek") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] > 10 RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with range seek (reverse)") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE 10 > n['key-' + n.name] RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test( - "warn for unfulfillable index seek when using dynamic property lookup with a single label and property existence check with exists") - { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE exists(n['na' + 'me']) RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with a single label and starts with") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] STARTS WITH 'Foo' RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with a single label and regex") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] =~ 'Foo*' RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with a single label and IN") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person) WHERE n['key-' + n.name] IN ['Foo', 'Bar'] RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with multiple labels") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n:Person:Foo) WHERE n['key-' + n.name] = 'value' RETURN n") - - result.notifications should contain(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava))) - } - - test("warn for unfulfillable index seek when using dynamic property lookup with multiple indexed labels") { - graph.createIndex("Person", "name") - graph.createIndex("Jedi", "weapon") - - val result = innerExecute("EXPLAIN MATCH (n:Person:Jedi) WHERE n['key-' + n.name] = 'value' RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person", "Jedi").asJava)))) - } - - test("should not warn when using dynamic property lookup with no labels") { - graph.createIndex("Person", "name") - - val result = innerExecute("EXPLAIN MATCH (n) WHERE n['key-' + n.name] = 'value' RETURN n") - - result.notifications shouldBe empty - } - - test("should warn when using dynamic property lookup with both a static and a dynamic property") { - graph.createIndex("Person", "name") - - val result = innerExecute( - "EXPLAIN MATCH (n:Person) WHERE n.name = 'Tobias' AND n['key-' + n.name] = 'value' RETURN n") - - result.notifications should equal(Set(INDEX_LOOKUP_FOR_DYNAMIC_PROPERTY.notification(graphdb.InputPosition.empty, - indexSeekOrScan( - Set("Person").asJava)))) - } - - test("should not warn when using dynamic property lookup with a label having no index") { - graph.createIndex("Person", "name") - createLabeledNode("Foo") - - val result = innerExecute("EXPLAIN MATCH (n:Foo) WHERE n['key-' + n.name] = 'value' RETURN n") - - result.notifications shouldBe empty - } - - test("should not warn for eager before load csv") { - val result = innerExecute( - "EXPLAIN MATCH (n) DELETE n WITH * LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MERGE () RETURN line") - - result should use("LoadCSV", "Eager") - result.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.EagerOperatorWarning" - } - - test("should warn for eager after load csv") { - val result = innerExecute( - "EXPLAIN MATCH (n) LOAD CSV FROM 'file:///ignore/ignore.csv' AS line WITH * DELETE n MERGE () RETURN line") - - result should use("LoadCSV", "Eager") - result.notifications.map(_.getCode) should contain("Neo.ClientNotification.Statement.EagerOperatorWarning") - } - - test("should not warn for load csv without eager") { - val result = innerExecute( - "EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MATCH (:A) CREATE (:B) RETURN line") - - result should use("LoadCSV") - result.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.EagerOperatorWarning" - } - - test("should not warn for eager without load csv") { - val result = innerExecute("EXPLAIN MATCH (a), (b) CREATE (c) RETURN *") - - result should use("Eager") - result.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.EagerOperatorWarning" - } - - test("should not warn for eager that precedes load csv") { - val result = innerExecute( - "EXPLAIN MATCH (a), (b) CREATE (c) WITH c LOAD CSV FROM 'file:///ignore/ignore.csv' AS line RETURN *") - - result should use("LoadCSV", "Eager") - result.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.EagerOperatorWarning" - } - - test("should warn for large label scans combined with load csv") { - 1 to 11 foreach { _ => createLabeledNode("A") } - val result = innerExecute("EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MATCH (a:A) RETURN *") - result should use("LoadCSV", "NodeByLabelScan") - result.notifications.map(_.getCode) should contain("Neo.ClientNotification.Statement.NoApplicableIndexWarning") - } - - test("should warn for large label scans with merge combined with load csv") { - 1 to 11 foreach { _ => createLabeledNode("A") } - val result = innerExecute("EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MERGE (a:A) RETURN *") - result should use("LoadCSV", "AntiConditionalApply") - result.notifications.map(_.getCode) should contain("Neo.ClientNotification.Statement.NoApplicableIndexWarning") - } - - test("should not warn for small label scans combined with load csv") { - createLabeledNode("A") - val result = innerExecute("EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MATCH (a:A) RETURN *") - result should use("LoadCSV", "NodeByLabelScan") - result.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.NoApplicableIndexWarning" - } - - test("should not warn for small label scans with merge combined with load csv") { - createLabeledNode("A") - val result = innerExecute("EXPLAIN LOAD CSV FROM 'file:///ignore/ignore.csv' AS line MERGE (a:A) RETURN *") - result should use("LoadCSV", "AntiConditionalApply") - result.notifications.map(_.getCode) should not contain "Neo.ClientNotification.Statement.NoApplicableIndexWarning" - } - - test("should warn for misspelled/missing label") { - //given - createLabeledNode("Person") - - //when - val resultMisspelled = innerExecute("EXPLAIN MATCH (n:Preson) RETURN *") - val resultCorrectlySpelled = innerExecute("EXPLAIN MATCH (n:Person) RETURN *") - - //then - resultMisspelled.notifications should contain( - MISSING_LABEL.notification(new graphdb.InputPosition(17, 1, 18), label("Preson"))) - - resultCorrectlySpelled.notifications shouldBe empty - } - - test("should not warn for missing label on update") { - - //when - val result = innerExecute("EXPLAIN CREATE (n:Person)") - - //then - result.notifications shouldBe empty - } - - test("should warn for misspelled/missing relationship type") { - //given - relate(createNode(), createNode(), "R") - - //when - val resultMisspelled = innerExecute("EXPLAIN MATCH ()-[r:r]->() RETURN *") - val resultCorrectlySpelled = innerExecute("EXPLAIN MATCH ()-[r:R]->() RETURN *") - - resultMisspelled.notifications should contain( - MISSING_REL_TYPE - .notification(new graphdb.InputPosition(20, 1, 21), NotificationDetail.Factory.relationshipType("r"))) - - resultCorrectlySpelled.notifications shouldBe empty - } - - test("should warn for misspelled/missing property names") { - //given - createNode(Map("prop" -> 42)) - //when - val resultMisspelled = innerExecute("EXPLAIN MATCH (n) WHERE n.propp = 43 RETURN n") - val resultCorrectlySpelled = innerExecute("EXPLAIN MATCH (n) WHERE n.prop = 43 RETURN n") - - resultMisspelled.notifications should contain( - NotificationCode.MISSING_PROPERTY_NAME.notification(new graphdb.InputPosition(26, 1, 27), propertyName("propp"))) - - resultCorrectlySpelled.notifications shouldBe empty - } - - test("should not warn for missing properties on update") { - val result = innerExecute("EXPLAIN CREATE (n {prop: 42})") - - result.notifications shouldBe empty - } - - test("should warn about unbounded shortest path") { - val res = innerExecute("EXPLAIN MATCH p = shortestPath((n)-[*]->(m)) RETURN m") - - res.notifications should contain( - UNBOUNDED_SHORTEST_PATH.notification(new graphdb.InputPosition(34, 1, 35))) - } - - test("2.3 can warn about bare nodes") { - val res = innerExecute("EXPLAIN CYPHER 2.3 MATCH n RETURN n") - - res.notifications should not be empty - } - - test("should not warn about literal maps") { - val res = innerExecute("explain return { id: 42 } ") - - res.notifications should be(empty) - } - - test("do not warn when creating a node with non-existent label when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE (n:Category)") - - result.notifications shouldBe empty - } - - test("do not warn when merging a node with non-existent label when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row MERGE (n:Category)") - - result.notifications shouldBe empty - } - - test("do not warn when setting on a node a non-existent label when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE (n) SET n:Category") - - result.notifications shouldBe empty - } - - test("do not warn when creating a rel with non-existent type when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE ()-[:T]->()") - - result.notifications shouldBe empty - } - - test("do not warn when merging a rel with non-existent type when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row MERGE ()-[:T]->()") - - result.notifications shouldBe empty - } - - test("do not warn when creating a node with non-existent prop key id when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row CREATE (n) SET n.p = 'a'") - - result.notifications shouldBe empty - } - - test("do not warn when merging a node with non-existent prop key id when using load csv") { - val result = innerExecute( - "EXPLAIN LOAD CSV WITH HEADERS FROM 'file:///fake.csv' AS row MERGE (n) ON CREATE SET n.p = 'a'") - - result.notifications shouldBe empty - } - - test("warn for use of deprecated toInt") { - val result = innerExecute("EXPLAIN RETURN toInt('1') AS one") - - result.notifications should contain(DEPRECATED_FUNCTION.notification(new graphdb.InputPosition(15, 1, 16), - deprecatedName("toInt", "toInteger")) - ) - } - - test("warn for use of deprecated upper") { - val result = innerExecute("EXPLAIN RETURN upper('foo') AS one") - - result.notifications should contain(DEPRECATED_FUNCTION.notification(new graphdb.InputPosition(15, 1, 16), - deprecatedName("upper", "toUpper"))) - } - - test("warn for use of deprecated lower") { - val result = innerExecute("EXPLAIN RETURN lower('BAR') AS one") - - result.notifications should contain(DEPRECATED_FUNCTION.notification(new graphdb.InputPosition(15, 1, 16), - deprecatedName("lower", "toLower"))) - } - - test("warn for use of deprecated rels") { - val result = innerExecute("EXPLAIN MATCH p = ()-->() RETURN rels(p) AS r") - - result.notifications should contain( - DEPRECATED_FUNCTION.notification(new graphdb.InputPosition(33, 1, 34), - deprecatedName("rels", "relationships"))) - } -} - -object NotificationAcceptanceTest { - - class TestProcedures { - - @Procedure("newProc") - def newProc(): Unit = {} - - @Deprecated - @Procedure(name = "oldProc", deprecatedBy = "newProc") - def oldProc(): Unit = {} - - @Procedure("changedProc") - def changedProc(): java.util.stream.Stream[ChangedResults] = - java.util.stream.Stream.builder().add(new ChangedResults).build() - } - -}