Skip to content

Added inspection to show warnings from explain queries #18

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 10, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.neueda.jetbrains.plugin.graphdb.database.api.query;

public interface GraphQueryNotification {

String getTitle();

String getDescription();

Integer getPositionOffset();
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ public interface GraphQueryResult {
List<GraphNode> getNodes();

List<GraphRelationship> getRelationships();

List<GraphQueryNotification> getNotifications();
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,22 @@

import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphNode;
import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphRelationship;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryNotification;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultColumn;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultRow;
import com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data.Neo4jBoltQueryNotification;
import com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data.Neo4jBoltQueryResultColumn;
import com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data.Neo4jBoltQueryResultRow;
import org.neo4j.driver.v1.summary.InputPosition;
import org.neo4j.driver.v1.summary.ResultSummary;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.Optional;

import static java.util.stream.Collectors.toList;

public class Neo4jBoltBuffer {

Expand All @@ -20,6 +26,7 @@ public class Neo4jBoltBuffer {
private ResultSummary resultSummary;
private List<GraphNode> nodes;
private List<GraphRelationship> relationships;
private List<GraphQueryNotification> notifications;

public Neo4jBoltBuffer() {
this.rows = new ArrayList<>();
Expand All @@ -28,7 +35,7 @@ public Neo4jBoltBuffer() {
public void addColumns(List<String> columns) {
this.columns = columns.stream()
.map(Neo4jBoltQueryResultColumn::new)
.collect(Collectors.toList());
.collect(toList());
}

public void addResultSummary(ResultSummary resultSummary) {
Expand All @@ -55,7 +62,7 @@ public List<GraphNode> getNodes() {
nodes = rows.stream()
.flatMap(row -> row.getNodes().stream())
.distinct()
.collect(Collectors.toList());
.collect(toList());

return nodes;
}
Expand All @@ -68,12 +75,32 @@ public List<GraphRelationship> getRelationships() {
relationships = rows.stream()
.flatMap(row -> row.getRelationships().stream())
.distinct()
.collect(Collectors.toList());
.collect(toList());

return relationships;
}

public ResultSummary getResultSummary() {
return resultSummary;
}

public List<GraphQueryNotification> getNotifications() {
if (resultSummary == null) {
return Collections.emptyList();
}

if (notifications != null) {
return notifications;
}

notifications = resultSummary.notifications().stream()
.map(notification -> new Neo4jBoltQueryNotification(notification.title(),
notification.description(),
Optional.ofNullable(notification.position())
.map(InputPosition::offset)
.orElse(null)))
.collect(toList());

return notifications;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.neueda.jetbrains.plugin.graphdb.database.neo4j.bolt.data;

import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryNotification;

public class Neo4jBoltQueryNotification implements GraphQueryNotification {

private String title;
private String description;
private Integer positionOffset;

public Neo4jBoltQueryNotification(String title, String description, Integer positionOffset) {
this.title = title;
this.description = description;
this.positionOffset = positionOffset;
}

@Override
public String getTitle() {
return title;
}

@Override
public String getDescription() {
return description;
}

@Override
public Integer getPositionOffset() {
return positionOffset;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphNode;
import com.neueda.jetbrains.plugin.graphdb.database.api.data.GraphRelationship;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryNotification;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResult;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultColumn;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResultRow;
Expand Down Expand Up @@ -143,6 +144,11 @@ public List<GraphRelationship> getRelationships() {
return buffer.getRelationships();
}

@Override
public List<GraphQueryNotification> getNotifications() {
return buffer.getNotifications();
}

private Optional<GraphNode> findNodeById(List<GraphNode> nodes, String id) {
return nodes.stream().filter((node) -> node.getId().equals(id)).findFirst();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,10 @@
<projectService serviceInterface="com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderService"
serviceImplementation="com.neueda.jetbrains.plugin.graphdb.language.cypher.completion.metadata.CypherMetadataProviderServiceImpl"/>
<lang.documentationProvider language="Cypher" implementationClass="com.neueda.jetbrains.plugin.graphdb.language.cypher.documentation.CypherDocumentationProvider"/>

<localInspection language="Cypher" displayName="Cypher EXPLAIN warning inspection" groupPath="Cypher"
groupName="General" enabledByDefault="true" level="WARNING"
implementationClass="com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection"/>
</extensions>

<actions>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<body>
<!-- tooltip end -->
Executes EXPLAIN query against active data source.
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.inspection;

import com.intellij.psi.PsiFile;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.component.datasource.state.DataSourceApi;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.util.NameUtil;
import com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.util.base.BaseIntegrationTest;

public class CypherExplainWarningInspectionTest extends BaseIntegrationTest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are go going to add more inspections in future. Let's extract some of the logic in "BaseInspectionTest"

Logic to extract:

  • Inspection class to enable
  • Configure from existing existing file + check highligting
  • Create data source file + add file to project


private DataSourceApi dsApi;

@Override
public void setUp() throws Exception {
super.setUp();
this.dsApi = dataSource().neo4j31();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to include neo4j30() data source in tests? Some of my tests are doing multi-neo4j-version tests.

This is not a requirement, more like general question.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, dataSource here is used just to get an instance of data source api, so we could easily generate file name for data source specific cypher file.
We could remove it, but then the file name generation would be done somewhere else.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are wrong. Data source is used not only for generation, but also for Query execution. You need to get actual notification somewhere :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, true. Completely forgot about that, while was writing comment :)

myFixture.enableInspections(CypherExplainWarningInspection.class);
}

public void testNonDataSourceFile_NoHighlight() {
PsiFile psiFile = myFixture.addFileToProject("a.cyp", "MATCH (a)-->(b) RETURN *");
myFixture.configureFromExistingVirtualFile(psiFile.getVirtualFile());
myFixture.checkHighlighting();
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be test that check behaviour if we are in data source bound file, but there are no actual data source,

public void testDataSourceFile_NoHighlight() {
String fileName = NameUtil.createDataSourceFileName(dsApi);
PsiFile psiFile = myFixture.addFileToProject(fileName,
"MATCH (a)-->(b) RETURN *");
myFixture.configureFromExistingVirtualFile(psiFile.getVirtualFile());
myFixture.checkHighlighting();
}

public void testDataSourceFile_HighlightExplainWarning() {
String fileName = NameUtil.createDataSourceFileName(dsApi);
PsiFile psiFile = myFixture.addFileToProject(fileName,
"MATCH (a)-[r:" +
"<warning descr=\"The provided relationship type is not in the database.\">" +
"ART</warning>]-(b) RETURN *;");
myFixture.configureFromExistingVirtualFile(psiFile.getVirtualFile());
myFixture.checkHighlighting();
}

public void testDataSourceFile_NoHighlightQueryError() {
String fileName = NameUtil.createDataSourceFileName(dsApi);
PsiFile psiFile = myFixture.addFileToProject(fileName,
"MATCH (a)-->() RETURN b;");
myFixture.configureFromExistingVirtualFile(psiFile.getVirtualFile());
myFixture.checkHighlighting();
}

public void testDataSourceFile_NoHighlightParserError() {
String fileName = NameUtil.createDataSourceFileName(dsApi);
PsiFile psiFile = myFixture.addFileToProject(fileName,
"MATCH a<error>-</error>->() RETURN *;");
myFixture.configureFromExistingVirtualFile(psiFile.getVirtualFile());
myFixture.checkHighlighting();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection;

import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.LocalInspectionToolSession;
import com.intellij.codeInspection.ProblemsHolder;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.neueda.jetbrains.plugin.graphdb.database.api.GraphDatabaseApi;
import com.neueda.jetbrains.plugin.graphdb.database.api.query.GraphQueryResult;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.component.datasource.DataSourcesComponent;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.database.DatabaseManagerService;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.util.NameUtil;
import com.neueda.jetbrains.plugin.graphdb.language.cypher.psi.CypherTypes;
import com.neueda.jetbrains.plugin.graphdb.platform.GraphConstants;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.neo4j.driver.v1.exceptions.ClientException;

import java.util.Objects;
import java.util.Optional;

public class CypherExplainWarningInspection extends LocalInspectionTool {

private DatabaseManagerService service;

public CypherExplainWarningInspection() {
this.service = ServiceManager.getService(DatabaseManagerService.class);
}

@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly,
@NotNull LocalInspectionToolSession session) {
return new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
checkStatement(element, holder);
}
};
}

@NotNull
@Override
public PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
return new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
checkStatement(element, holder);
}
};
}

private void checkStatement(@NotNull PsiElement statement, @NotNull ProblemsHolder problemsHolder) {
if (statement.getNode().getElementType() == CypherTypes.SINGLE_QUERY) {

String fileName = statement.getContainingFile().getName();
if (fileName.startsWith(GraphConstants.BOUND_DATA_SOURCE_PREFIX)) {
DataSourcesComponent component = statement.getProject().getComponent(DataSourcesComponent.class);

component.getDataSourceContainer()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

streams ftw

.findDataSource(NameUtil.extractDataSourceUUID(fileName))
.map(service::getDatabaseFor)
.map(api -> this.executeExplainQuery(api, statement.getText()))
.filter(Objects::nonNull)
.map(GraphQueryResult::getNotifications)
.filter(list -> !list.isEmpty())
.ifPresent(notifications -> notifications.forEach(notification -> {
PsiElement elementAt = Optional.ofNullable(notification.getPositionOffset())
.filter(position -> position > 0)
.map(statement::findElementAt)
.orElse(statement);

problemsHolder.registerProblem(elementAt, notification.getTitle());
}));
}
}
}

@Nullable
@Override
public String loadDescription() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this accidentally added code

return super.loadDescription();
}

private GraphQueryResult executeExplainQuery(GraphDatabaseApi api, String query) {
try {
return api.execute("EXPLAIN " + query);
} catch (ClientException ex) {
return null;
}
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this empty line

}