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 all commits
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,55 @@
package com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.inspection;

import com.intellij.codeInspection.LocalInspectionTool;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.inspection.CypherExplainWarningInspection;
import com.neueda.jetbrains.plugin.graphdb.platform.GraphConstants;
import com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.util.BaseInspectionTest;

import java.util.Set;

import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;

public class CypherExplainWarningInspectionTest extends BaseInspectionTest {

@Override
protected Set<Class<? extends LocalInspectionTool>> provideInspectionClasses() {
return singleton(CypherExplainWarningInspection.class);
}

public void testNonDataSourceFile_NoHighlight() {
addFileAndCheck("a.cyp", "MATCH (a)-->(b) RETURN *");
}

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() {
addDataSourceFileAndCheck("MATCH (a)-->(b) RETURN *");
}

public void testDataSourceFile_HighlightExplainWarning() {
addDataSourceFileAndCheck("MATCH (a)-[r:" +
"<warning descr=\"The provided relationship type is not in the database.\">" +
"ART</warning>]-(b) RETURN *;");
}

public void testDataSourceFile_NoHighlightQueryError() {
addDataSourceFileAndCheck("MATCH (a)-->() RETURN b;");
}

public void testDataSourceFile_NoHighlightParserError() {
addDataSourceFileAndCheck("MATCH a<error>-</error>->() RETURN *;");
}

public void testDataSourceFile_NoDataSource() {
component().dataSources().getDataSourceContainer().removeDataSources(singletonList(dataSource().neo4j31()));
addFileAndCheck(GraphConstants.BOUND_DATA_SOURCE_PREFIX + "imaginary-ds-uuid-with-36-symbols-in.cypher",
"MATCH (a:Turbo)-->() RETURN *;");
}

public void testDataSourceFile_UserCreatedDSLikeFile() {
component().dataSources().getDataSourceContainer().removeDataSources(singletonList(dataSource().neo4j31()));
// uuid should be 36 symbols long, let's assume user created a file with a name, starting like ds file
// but does not match the expected format
addFileAndCheck(GraphConstants.BOUND_DATA_SOURCE_PREFIX + "ds-uuid-with-23-symbols.cypher",
"MATCH (a:Turbo)-->() RETURN *;");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.tests.cypher.util;

import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.neueda.jetbrains.plugin.graphdb.jetbrains.util.NameUtil;
import com.neueda.jetbrains.plugin.graphdb.test.integration.neo4j.util.base.BaseIntegrationTest;

import java.util.Optional;
import java.util.Set;

public abstract class BaseInspectionTest extends BaseIntegrationTest {
private String dsApiUUID;

@Override
public void setUp() throws Exception {
super.setUp();
this.dsApiUUID = dataSource().neo4j31().getUUID();
myFixture.enableInspections(provideInspectionClasses());
}

protected abstract Set<Class<? extends LocalInspectionTool>> provideInspectionClasses();

protected void addFileAndCheck(String filePath, String fileContent) {
PsiFile psiFile = myFixture.addFileToProject(filePath, fileContent);
configureAndCheck(psiFile.getVirtualFile());
}

protected void addDataSourceFileAndCheck(String fileContent) {
String fileName = Optional.of(dsApiUUID)
.flatMap(uuid -> component().dataSources().getDataSourceContainer().findDataSource(uuid))
.map(NameUtil::createDataSourceFileName)
.orElseThrow(IllegalStateException::new);

addFileAndCheck(fileName, fileContent);
}

private void configureAndCheck(VirtualFile virtualFile) {
myFixture.configureFromExistingVirtualFile(virtualFile);
myFixture.checkHighlighting();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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.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) {
Optional.of(statement.getContainingFile().getName())
.filter(s -> s.startsWith(GraphConstants.BOUND_DATA_SOURCE_PREFIX))
.map(this::safeExtractDataSourceUUID)
.flatMap(uuid -> statement.getProject()
.getComponent(DataSourcesComponent.class)
.getDataSourceContainer()
.findDataSource(uuid))
.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());
}));
}
}

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

private String safeExtractDataSourceUUID(String fileName) {
try {
return NameUtil.extractDataSourceUUID(fileName);
} catch (IndexOutOfBoundsException e) {
return null;
}
}
}