Skip to content

Commit ec91bd2

Browse files
MetaurFylmTM
authored andcommitted
Added inspection to show warnings from explain queries (#18)
* Added inspection to show warnings from explain queries * Refactored tests, review fixes
1 parent 4516070 commit ec91bd2

File tree

10 files changed

+280
-4
lines changed

10 files changed

+280
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.neueda.jetbrains.plugin.graphdb.database.api.query;
2+
3+
public interface GraphQueryNotification {
4+
5+
String getTitle();
6+
7+
String getDescription();
8+
9+
Integer getPositionOffset();
10+
}

database/api/src/main/java/com/neueda/jetbrains/plugin/graphdb/database/api/query/GraphQueryResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,6 @@ public interface GraphQueryResult {
1818
List<GraphNode> getNodes();
1919

2020
List<GraphRelationship> getRelationships();
21+
22+
List<GraphQueryNotification> getNotifications();
2123
}

database/neo4j/src/main/java/com/neueda/jetbrains/plugin/graphdb/database/neo4j/bolt/Neo4jBoltBuffer.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22

33
import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphNode;
44
import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphRelationship;
5+
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryNotification;
56
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultColumn;
67
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultRow;
8+
import com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data.Neo4jBoltQueryNotification;
79
import com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data.Neo4jBoltQueryResultColumn;
810
import com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data.Neo4jBoltQueryResultRow;
11+
import org.neo4j.driver.v1.summary.InputPosition;
912
import org.neo4j.driver.v1.summary.ResultSummary;
1013

1114
import java.util.ArrayList;
15+
import java.util.Collections;
1216
import java.util.List;
1317
import java.util.Map;
14-
import java.util.stream.Collectors;
18+
import java.util.Optional;
19+
20+
import static java.util.stream.Collectors.toList;
1521

1622
public class Neo4jBoltBuffer {
1723

@@ -20,6 +26,7 @@ public class Neo4jBoltBuffer {
2026
private ResultSummary resultSummary;
2127
private List<GraphNode> nodes;
2228
private List<GraphRelationship> relationships;
29+
private List<GraphQueryNotification> notifications;
2330

2431
public Neo4jBoltBuffer() {
2532
this.rows = new ArrayList<>();
@@ -28,7 +35,7 @@ public Neo4jBoltBuffer() {
2835
public void addColumns(List<String> columns) {
2936
this.columns = columns.stream()
3037
.map(Neo4jBoltQueryResultColumn::new)
31-
.collect(Collectors.toList());
38+
.collect(toList());
3239
}
3340

3441
public void addResultSummary(ResultSummary resultSummary) {
@@ -55,7 +62,7 @@ public List<GraphNode> getNodes() {
5562
nodes = rows.stream()
5663
.flatMap(row -> row.getNodes().stream())
5764
.distinct()
58-
.collect(Collectors.toList());
65+
.collect(toList());
5966

6067
return nodes;
6168
}
@@ -68,12 +75,32 @@ public List<GraphRelationship> getRelationships() {
6875
relationships = rows.stream()
6976
.flatMap(row -> row.getRelationships().stream())
7077
.distinct()
71-
.collect(Collectors.toList());
78+
.collect(toList());
7279

7380
return relationships;
7481
}
7582

7683
public ResultSummary getResultSummary() {
7784
return resultSummary;
7885
}
86+
87+
public List<GraphQueryNotification> getNotifications() {
88+
if (resultSummary == null) {
89+
return Collections.emptyList();
90+
}
91+
92+
if (notifications != null) {
93+
return notifications;
94+
}
95+
96+
notifications = resultSummary.notifications().stream()
97+
.map(notification -> new Neo4jBoltQueryNotification(notification.title(),
98+
notification.description(),
99+
Optional.ofNullable(notification.position())
100+
.map(InputPosition::offset)
101+
.orElse(null)))
102+
.collect(toList());
103+
104+
return notifications;
105+
}
79106
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data;
2+
3+
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryNotification;
4+
5+
public class Neo4jBoltQueryNotification implements GraphQueryNotification {
6+
7+
private String title;
8+
private String description;
9+
private Integer positionOffset;
10+
11+
public Neo4jBoltQueryNotification(String title, String description, Integer positionOffset) {
12+
this.title = title;
13+
this.description = description;
14+
this.positionOffset = positionOffset;
15+
}
16+
17+
@Override
18+
public String getTitle() {
19+
return title;
20+
}
21+
22+
@Override
23+
public String getDescription() {
24+
return description;
25+
}
26+
27+
@Override
28+
public Integer getPositionOffset() {
29+
return positionOffset;
30+
}
31+
}

database/neo4j/src/main/java/com/neueda/jetbrains/plugin/graphdb/database/neo4j/bolt/query/Neo4jBoltQueryResult.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphNode;
44
import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphRelationship;
5+
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryNotification;
56
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResult;
67
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultColumn;
78
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultRow;
@@ -143,6 +144,11 @@ public List<GraphRelationship> getRelationships() {
143144
return buffer.getRelationships();
144145
}
145146

147+
@Override
148+
public List<GraphQueryNotification> getNotifications() {
149+
return buffer.getNotifications();
150+
}
151+
146152
private Optional<GraphNode> findNodeById(List<GraphNode> nodes, String id) {
147153
return nodes.stream().filter((node) -> node.getId().equals(id)).findFirst();
148154
}

graph-database-support-plugin/src/main/resources/META-INF/plugin.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@
150150
<projectService serviceInterface="com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderService"
151151
serviceImplementation="com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderServiceImpl"/>
152152
<lang.documentationProvider language="Cypher" implementationClass="com.neueda.jetbrains.plugin.graphdb.language.cypher.documentation.CypherDocumentationProvider"/>
153+
154+
<localInspection language="Cypher" displayName="Cypher EXPLAIN warning inspection" groupPath="Cypher"
155+
groupName="General" enabledByDefault="true" level="WARNING"
156+
implementationClass="com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection"/>
153157
</extensions>
154158

155159
<actions>
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<html>
2+
<body>
3+
<!-- tooltip end -->
4+
Executes EXPLAIN query against active data source.
5+
</body>
6+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.inspection;
2+
3+
import com.intellij.codeInspection.LocalInspectionTool;
4+
import com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection;
5+
import com.neueda.jetbrains.plugin.graphdb.platform.GraphConstants;
6+
import com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.util.BaseInspectionTest;
7+
8+
import java.util.Set;
9+
10+
import static java.util.Collections.singleton;
11+
import static java.util.Collections.singletonList;
12+
13+
public class CypherExplainWarningInspectionTest extends BaseInspectionTest {
14+
15+
@Override
16+
protected Set<Class<? extends LocalInspectionTool>> provideInspectionClasses() {
17+
return singleton(CypherExplainWarningInspection.class);
18+
}
19+
20+
public void testNonDataSourceFile_NoHighlight() {
21+
addFileAndCheck("a.cyp", "MATCH (a)-->(b) RETURN *");
22+
}
23+
24+
public void testDataSourceFile_NoHighlight() {
25+
addDataSourceFileAndCheck("MATCH (a)-->(b) RETURN *");
26+
}
27+
28+
public void testDataSourceFile_HighlightExplainWarning() {
29+
addDataSourceFileAndCheck("MATCH (a)-[r:" +
30+
"<warning descr=\"The provided relationship type is not in the database.\">" +
31+
"ART</warning>]-(b) RETURN *;");
32+
}
33+
34+
public void testDataSourceFile_NoHighlightQueryError() {
35+
addDataSourceFileAndCheck("MATCH (a)-->() RETURN b;");
36+
}
37+
38+
public void testDataSourceFile_NoHighlightParserError() {
39+
addDataSourceFileAndCheck("MATCH a<error>-</error>->() RETURN *;");
40+
}
41+
42+
public void testDataSourceFile_NoDataSource() {
43+
component().dataSources().getDataSourceContainer().removeDataSources(singletonList(dataSource().neo4j31()));
44+
addFileAndCheck(GraphConstants.BOUND_DATA_SOURCE_PREFIX + "imaginary-ds-uuid-with-36-symbols-in.cypher",
45+
"MATCH (a:Turbo)-->() RETURN *;");
46+
}
47+
48+
public void testDataSourceFile_UserCreatedDSLikeFile() {
49+
component().dataSources().getDataSourceContainer().removeDataSources(singletonList(dataSource().neo4j31()));
50+
// uuid should be 36 symbols long, let's assume user created a file with a name, starting like ds file
51+
// but does not match the expected format
52+
addFileAndCheck(GraphConstants.BOUND_DATA_SOURCE_PREFIX + "ds-uuid-with-23-symbols.cypher",
53+
"MATCH (a:Turbo)-->() RETURN *;");
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.util;
2+
3+
import com.intellij.codeInspection.LocalInspectionTool;
4+
import com.intellij.openapi.vfs.VirtualFile;
5+
import com.intellij.psi.PsiFile;
6+
import com.neueda.jetbrains.plugin.graphdb.jetbrains.util.NameUtil;
7+
import com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.util.base.BaseIntegrationTest;
8+
9+
import java.util.Optional;
10+
import java.util.Set;
11+
12+
public abstract class BaseInspectionTest extends BaseIntegrationTest {
13+
private String dsApiUUID;
14+
15+
@Override
16+
public void setUp() throws Exception {
17+
super.setUp();
18+
this.dsApiUUID = dataSource().neo4j31().getUUID();
19+
myFixture.enableInspections(provideInspectionClasses());
20+
}
21+
22+
protected abstract Set<Class<? extends LocalInspectionTool>> provideInspectionClasses();
23+
24+
protected void addFileAndCheck(String filePath, String fileContent) {
25+
PsiFile psiFile = myFixture.addFileToProject(filePath, fileContent);
26+
configureAndCheck(psiFile.getVirtualFile());
27+
}
28+
29+
protected void addDataSourceFileAndCheck(String fileContent) {
30+
String fileName = Optional.of(dsApiUUID)
31+
.flatMap(uuid -> component().dataSources().getDataSourceContainer().findDataSource(uuid))
32+
.map(NameUtil::createDataSourceFileName)
33+
.orElseThrow(IllegalStateException::new);
34+
35+
addFileAndCheck(fileName, fileContent);
36+
}
37+
38+
private void configureAndCheck(VirtualFile virtualFile) {
39+
myFixture.configureFromExistingVirtualFile(virtualFile);
40+
myFixture.checkHighlighting();
41+
}
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection;
2+
3+
import com.intellij.codeInspection.LocalInspectionTool;
4+
import com.intellij.codeInspection.LocalInspectionToolSession;
5+
import com.intellij.codeInspection.ProblemsHolder;
6+
import com.intellij.openapi.components.ServiceManager;
7+
import com.intellij.psi.PsiElement;
8+
import com.intellij.psi.PsiElementVisitor;
9+
import com.neueda.jetbrains.plugin.graphdb.database.api.GraphDatabaseApi;
10+
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResult;
11+
import com.neueda.jetbrains.plugin.graphdb.jetbrains.component.datasource.DataSourcesComponent;
12+
import com.neueda.jetbrains.plugin.graphdb.jetbrains.database.DatabaseManagerService;
13+
import com.neueda.jetbrains.plugin.graphdb.jetbrains.util.NameUtil;
14+
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherTypes;
15+
import com.neueda.jetbrains.plugin.graphdb.platform.GraphConstants;
16+
import org.jetbrains.annotations.NotNull;
17+
import org.neo4j.driver.v1.exceptions.ClientException;
18+
19+
import java.util.Objects;
20+
import java.util.Optional;
21+
22+
public class CypherExplainWarningInspection extends LocalInspectionTool {
23+
24+
private DatabaseManagerService service;
25+
26+
public CypherExplainWarningInspection() {
27+
this.service = ServiceManager.getService(DatabaseManagerService.class);
28+
}
29+
30+
@NotNull
31+
@Override
32+
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly,
33+
@NotNull LocalInspectionToolSession session) {
34+
return new PsiElementVisitor() {
35+
@Override
36+
public void visitElement(PsiElement element) {
37+
checkStatement(element, holder);
38+
}
39+
};
40+
}
41+
42+
@NotNull
43+
@Override
44+
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
45+
return new PsiElementVisitor() {
46+
@Override
47+
public void visitElement(PsiElement element) {
48+
checkStatement(element, holder);
49+
}
50+
};
51+
}
52+
53+
private void checkStatement(@NotNull PsiElement statement, @NotNull ProblemsHolder problemsHolder) {
54+
if (statement.getNode().getElementType() == CypherTypes.SINGLE_QUERY) {
55+
Optional.of(statement.getContainingFile().getName())
56+
.filter(s -> s.startsWith(GraphConstants.BOUND_DATA_SOURCE_PREFIX))
57+
.map(this::safeExtractDataSourceUUID)
58+
.flatMap(uuid -> statement.getProject()
59+
.getComponent(DataSourcesComponent.class)
60+
.getDataSourceContainer()
61+
.findDataSource(uuid))
62+
.map(service::getDatabaseFor)
63+
.map(api -> this.executeExplainQuery(api, statement.getText()))
64+
.filter(Objects::nonNull)
65+
.map(GraphQueryResult::getNotifications)
66+
.filter(list -> !list.isEmpty())
67+
.ifPresent(notifications -> notifications.forEach(notification -> {
68+
PsiElement elementAt = Optional.ofNullable(notification.getPositionOffset())
69+
.filter(position -> position > 0)
70+
.map(statement::findElementAt)
71+
.orElse(statement);
72+
73+
problemsHolder.registerProblem(elementAt, notification.getTitle());
74+
}));
75+
}
76+
}
77+
78+
private GraphQueryResult executeExplainQuery(GraphDatabaseApi api, String query) {
79+
try {
80+
return api.execute("EXPLAIN " + query);
81+
} catch (ClientException ex) {
82+
return null;
83+
}
84+
}
85+
86+
private String safeExtractDataSourceUUID(String fileName) {
87+
try {
88+
return NameUtil.extractDataSourceUUID(fileName);
89+
} catch (IndexOutOfBoundsException e) {
90+
return null;
91+
}
92+
}
93+
}

0 commit comments

Comments
 (0)