Skip to content
This repository was archived by the owner on Jul 6, 2023. It is now read-only.

Commit 3dce3ec

Browse files
Merge pull request #219 from LinneaAndersson/4.0-plan-table-details-column
Render Details column in plan table
2 parents a1fb500 + 01073ed commit 3dce3ec

File tree

3 files changed

+123
-7
lines changed

3 files changed

+123
-7
lines changed

cypher-shell/src/integration-test/java/org/neo4j/shell/commands/CypherShellVerboseIntegrationTest.java

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.neo4j.shell.cli.Format;
1414
import org.neo4j.shell.exception.CommandException;
1515
import org.neo4j.shell.prettyprint.PrettyConfig;
16+
import org.neo4j.shell.prettyprint.TablePlanFormatter;
1617

1718
import static org.hamcrest.CoreMatchers.containsString;
1819
import static org.hamcrest.CoreMatchers.equalTo;
@@ -192,7 +193,7 @@ public void paramsAndListVariablesWithSpecialCharacters() throws EvaluationExcep
192193
public void cypherWithOrder() throws CommandException {
193194
// given
194195
String serverVersion = shell.getServerVersion();
195-
assumeTrue(minorVersion(serverVersion) == 6 || majorVersion(serverVersion) == 4);
196+
assumeTrue((minorVersion(serverVersion) == 6 && majorVersion(serverVersion) == 3) || majorVersion(serverVersion) > 3);
196197

197198
shell.execute( "CREATE INDEX ON :Person(age)" );
198199
shell.execute( "CALL db.awaitIndexes()" );
@@ -206,6 +207,37 @@ public void cypherWithOrder() throws CommandException {
206207
assertThat( actual, containsString( "n.age ASC" ) );
207208
}
208209

210+
@Test
211+
public void cypherWithQueryDetails() throws CommandException {
212+
// given
213+
String serverVersion = shell.getServerVersion();
214+
assumeTrue((minorVersion(serverVersion) > 0 && majorVersion(serverVersion) == 4) || majorVersion(serverVersion) > 4);
215+
216+
//when
217+
shell.execute("EXPLAIN MATCH (n) with n.age AS age RETURN age");
218+
219+
//then
220+
String actual = linePrinter.output();
221+
assertThat( actual, containsString( TablePlanFormatter.DETAILS ) );
222+
assertThat( actual, containsString( "n.age AS age" ) );
223+
assertThat( actual, not( containsString( TablePlanFormatter.IDENTIFIERS ) ) );
224+
}
225+
226+
@Test
227+
public void cypherWithoutQueryDetails() throws CommandException {
228+
// given
229+
String serverVersion = shell.getServerVersion();
230+
assumeTrue((minorVersion(serverVersion) == 0 && majorVersion(serverVersion) == 4) || majorVersion(serverVersion) < 4);
231+
232+
//when
233+
shell.execute("EXPLAIN MATCH (n) with n.age AS age RETURN age");
234+
235+
//then
236+
String actual = linePrinter.output();
237+
assertThat( actual, not( containsString( TablePlanFormatter.DETAILS ) ) );
238+
assertThat( actual, containsString( TablePlanFormatter.IDENTIFIERS ) );
239+
}
240+
209241
@Test
210242
public void cypherWithExplainAndRulePlanner() throws CommandException {
211243
//given (there is no rule planner in neo4j 4.0)

cypher-shell/src/main/java/org/neo4j/shell/prettyprint/TablePlanFormatter.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,9 @@
2626
import static org.neo4j.shell.prettyprint.OutputFormatter.NEWLINE;
2727
import static org.neo4j.shell.prettyprint.OutputFormatter.repeat;
2828

29-
class TablePlanFormatter {
29+
public class TablePlanFormatter {
3030

31-
private static final String UNNAMED_PATTERN_STRING = " (UNNAMED|FRESHID|AGGREGATION)(\\d+)";
31+
private static final String UNNAMED_PATTERN_STRING = " (UNNAMED|FRESHID|AGGREGATION|NODE|REL)(\\d+)";
3232
private static final Pattern UNNAMED_PATTERN = Pattern.compile(UNNAMED_PATTERN_STRING);
3333
private static final String OPERATOR = "Operator";
3434
private static final String ESTIMATED_ROWS = "Estimated Rows";
@@ -37,16 +37,18 @@ class TablePlanFormatter {
3737
private static final String PAGE_CACHE = "Cache H/M";
3838
private static final String TIME = "Time (ms)";
3939
private static final String ORDER = "Ordered by";
40-
private static final String IDENTIFIERS = "Identifiers";
40+
public static final String IDENTIFIERS = "Identifiers";
4141
private static final String OTHER = "Other";
42+
public static final String DETAILS = "Details";
4243
private static final String SEPARATOR = ", ";
4344
private static final Pattern DEDUP_PATTERN = Pattern.compile("\\s*(\\S+)@\\d+");
45+
public static final int MAX_DETAILS_COLUMN_WIDTH = 100;
4446

45-
private static final List<String> HEADERS = asList(OPERATOR, ESTIMATED_ROWS, ROWS, HITS, PAGE_CACHE, TIME, IDENTIFIERS, ORDER, OTHER);
47+
private static final List<String> HEADERS = asList(OPERATOR, DETAILS, ESTIMATED_ROWS, ROWS, HITS, PAGE_CACHE, TIME, IDENTIFIERS, ORDER, OTHER);
4648

4749
private static final Set<String> IGNORED_ARGUMENTS = new LinkedHashSet<>(
4850
asList( "Rows", "DbHits", "EstimatedRows", "planner", "planner-impl", "planner-version", "version", "runtime", "runtime-impl", "runtime-version",
49-
"time", "source-code", "PageCacheMisses", "PageCacheHits", "PageCacheHitRatio", "Order" ) );
51+
"time", "source-code", "PageCacheMisses", "PageCacheHits", "PageCacheHitRatio", "Order", "Details" ) );
5052
public static final Value ZERO_VALUE = Values.value(0);
5153

5254
private int width(@Nonnull String header, @Nonnull Map<String, Integer> columns) {
@@ -78,7 +80,8 @@ String formatPlan(@Nonnull Plan plan) {
7880
Map<String, Integer> columns = new HashMap<>();
7981
List<Line> lines = accumulate(plan, new Root(), columns);
8082

81-
List<String> headers = HEADERS.stream().filter(columns::containsKey).collect(Collectors.toList());
83+
// Remove Identifiers column if we have a Details column
84+
List<String> headers = HEADERS.stream().filter(header -> columns.containsKey(header) && !(header.equals(IDENTIFIERS) && columns.containsKey(DETAILS))).collect(Collectors.toList());
8285

8386
StringBuilder result = new StringBuilder((2 + NEWLINE.length() + headers.stream().mapToInt(h -> width(h, columns)).sum()) * (lines.size() * 2 + 3));
8487

@@ -160,6 +163,8 @@ private String serialize(@Nonnull String key, @Nonnull Value v) {
160163
return v.asString();
161164
case "PageCacheMisses":
162165
return v.asNumber().toString();
166+
case "Details":
167+
return v.asString();
163168
default:
164169
return v.asObject().toString();
165170
}
@@ -207,6 +212,8 @@ private Map<String, Justified> details(@Nonnull Plan plan, @Nonnull Map<String,
207212
return mapping(TIME, new Right(String.format("%.3f", value.asLong() / 1000000.0d)), columns);
208213
case "Order":
209214
return mapping( ORDER, new Left( String.format( "%s", value.asString() ) ), columns );
215+
case "Details":
216+
return mapping( DETAILS, new Left( truncate(value.asString()) ), columns );
210217
default:
211218
return Optional.empty();
212219
}
@@ -436,4 +443,12 @@ public static <T1, T2> Pair<T1, T2> of(T1 _1, T2 _2) {
436443
return new Pair<>(_1, _2);
437444
}
438445
}
446+
447+
private String truncate( String original ) {
448+
if(original.length() <= MAX_DETAILS_COLUMN_WIDTH){
449+
return original;
450+
}
451+
452+
return original.substring( 0, MAX_DETAILS_COLUMN_WIDTH - 3 ) + "...";
453+
}
439454
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.neo4j.shell.prettyprint;
2+
3+
import org.junit.Test;
4+
5+
import java.util.Collections;
6+
import java.util.Map;
7+
8+
import org.neo4j.driver.Value;
9+
import org.neo4j.driver.internal.value.StringValue;
10+
import org.neo4j.driver.summary.Plan;
11+
12+
import static org.hamcrest.CoreMatchers.containsString;
13+
import static org.hamcrest.CoreMatchers.is;
14+
import static org.hamcrest.MatcherAssert.assertThat;
15+
import static org.mockito.Mockito.mock;
16+
import static org.mockito.Mockito.when;
17+
import static org.neo4j.shell.prettyprint.OutputFormatter.NEWLINE;
18+
19+
public class TablePlanFormatterTest
20+
{
21+
TablePlanFormatter tablePlanFormatter = new TablePlanFormatter();
22+
23+
@Test
24+
public void renderShortDetails() {
25+
Plan plan = mock(Plan.class);
26+
Map<String, Value> args = Collections.singletonMap("Details", new StringValue("x.prop AS prop"));
27+
when(plan.arguments()).thenReturn(args);
28+
when(plan.operatorType()).thenReturn("Projection");
29+
30+
assertThat(tablePlanFormatter.formatPlan( plan ), is(String.join(NEWLINE,
31+
"+-------------+----------------+",
32+
"| Operator | Details |",
33+
"+-------------+----------------+",
34+
"| +Projection | x.prop AS prop |",
35+
"+-------------+----------------+", "")));
36+
}
37+
38+
@Test
39+
public void renderExactMaxLengthDetails() {
40+
Plan plan = mock(Plan.class);
41+
String details = stringOfLength(TablePlanFormatter.MAX_DETAILS_COLUMN_WIDTH);
42+
Map<String, Value> args = Collections.singletonMap("Details", new StringValue(details));
43+
when(plan.arguments()).thenReturn(args);
44+
when(plan.operatorType()).thenReturn("Projection");
45+
46+
assertThat(tablePlanFormatter.formatPlan( plan ), containsString("| +Projection | " + details + " |"));
47+
}
48+
49+
@Test
50+
public void truncateTooLongDetails() {
51+
Plan plan = mock(Plan.class);
52+
String details = stringOfLength(TablePlanFormatter.MAX_DETAILS_COLUMN_WIDTH + 1);
53+
Map<String, Value> args = Collections.singletonMap("Details", new StringValue(details));
54+
when(plan.arguments()).thenReturn(args);
55+
when(plan.operatorType()).thenReturn("Projection");
56+
57+
assertThat(tablePlanFormatter.formatPlan( plan ), containsString("| +Projection | " + details.substring( 0, TablePlanFormatter.MAX_DETAILS_COLUMN_WIDTH - 3 ) + "... |"));
58+
}
59+
60+
private String stringOfLength(int length) {
61+
StringBuilder strBuilder = new StringBuilder();
62+
63+
for(int i=0; i<length; i++) {
64+
strBuilder.append('a');
65+
}
66+
67+
return strBuilder.toString();
68+
}
69+
}

0 commit comments

Comments
 (0)