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()
- }
-
-}