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

Commit a28784b

Browse files
committed
Stream results of autocommit queries...
When executing queries in autocommit mode, results are no longer materialized before printing. This was causing issues for big queries that would materialize as OOM crashes or just generally slow execution. For --format=PLAIN this means each records is printed directly as it arrived in the shell. For --format=VERBOSE, we cannot simply stream, because we need to know the widths of the columns before we can print even the column header. To achieve this we materialize the 1000 first rows, and compute the column widths from this sample. Any remaining rows will be streamed. If any remaining row contains a value that overflows it's column width, that value will either be wrapped to the next row (--wrap=true), or truncated (--wrap=false)
1 parent ea28065 commit a28784b

23 files changed

+572
-313
lines changed

cypher-shell/src/main/java/org/neo4j/shell/CypherShell.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public class CypherShell implements StatementExecuter, Connector, TransactionHan
3434
protected CommandHelper commandHelper;
3535

3636
public CypherShell(@Nonnull Logger logger) {
37-
this(logger, new BoltStateHandler(), new PrettyPrinter(logger.getFormat()));
37+
this(logger, new BoltStateHandler(), new PrettyPrinter(logger.getFormat(), logger.getWrap(), logger.getNumSampleRows()));
3838
}
3939

4040
protected CypherShell(@Nonnull Logger logger,
@@ -83,7 +83,7 @@ public void execute(@Nonnull final String cmdString) throws ExitException, Comma
8383
*/
8484
protected void executeCypher(@Nonnull final String cypher) throws CommandException {
8585
final Optional<BoltResult> result = boltStateHandler.runCypher(cypher, allParameterValues());
86-
result.ifPresent(boltResult -> logger.printOut(prettyPrinter.format(boltResult)));
86+
result.ifPresent(boltResult -> prettyPrinter.format(boltResult, logger::printOut));
8787
}
8888

8989
@Override
@@ -138,7 +138,7 @@ public void beginTransaction() throws CommandException {
138138
@Override
139139
public Optional<List<BoltResult>> commitTransaction() throws CommandException {
140140
Optional<List<BoltResult>> results = boltStateHandler.commitTransaction();
141-
results.ifPresent(boltResult -> boltResult.forEach(result -> logger.printOut(prettyPrinter.format(result))));
141+
results.ifPresent(boltResult -> boltResult.forEach(result -> prettyPrinter.format(result, logger::printOut)));
142142
return results;
143143
}
144144

cypher-shell/src/main/java/org/neo4j/shell/Main.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ private Logger instantiateLogger(@Nonnull CliArgs cliArgs) {
9393
if (cliArgs.isStringShell() && Format.AUTO.equals(cliArgs.getFormat())) {
9494
logger.setFormat(Format.PLAIN);
9595
}
96+
logger.setWrap(cliArgs.getWrap());
97+
logger.setNumSampleRows(cliArgs.getNumSampleRows());
9698
return logger;
9799
}
98100

cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgHelper.java

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
import net.sourceforge.argparse4j.impl.action.StoreTrueArgumentAction;
66
import net.sourceforge.argparse4j.impl.choice.CollectionArgumentChoice;
77
import net.sourceforge.argparse4j.impl.type.BooleanArgumentType;
8-
import net.sourceforge.argparse4j.inf.ArgumentGroup;
9-
import net.sourceforge.argparse4j.inf.ArgumentParser;
10-
import net.sourceforge.argparse4j.inf.ArgumentParserException;
11-
import net.sourceforge.argparse4j.inf.MutuallyExclusiveGroup;
12-
import net.sourceforge.argparse4j.inf.Namespace;
8+
import net.sourceforge.argparse4j.inf.*;
139

1410
import java.io.PrintWriter;
1511
import java.util.regex.Matcher;
@@ -88,6 +84,10 @@ public static CliArgs parse(@Nonnull String... args) {
8884

8985
cliArgs.setNonInteractive(ns.getBoolean("force-non-interactive"));
9086

87+
cliArgs.setWrap(ns.getBoolean("wrap"));
88+
89+
cliArgs.setNumSampleRows(ns.getInt("sample-rows"));
90+
9191
cliArgs.setVersion(ns.getBoolean("version"));
9292

9393
cliArgs.setDriverVersion(ns.getBoolean("driver-version"));
@@ -167,6 +167,17 @@ private static ArgumentParser setupParser()
167167
.dest("force-non-interactive")
168168
.action(new StoreTrueArgumentAction());
169169

170+
parser.addArgument("--sample-rows")
171+
.help("number of rows sampled to compute table widths (only for format=VERBOSE)")
172+
.type(new PositiveIntegerType())
173+
.dest("sample-rows")
174+
.setDefault(CliArgs.DEFAULT_NUM_SAMPLE_ROWS);
175+
176+
parser.addArgument("--wrap")
177+
.help("wrap table colum values if column is too narrow (only for format=VERBOSE)")
178+
.type(new BooleanArgumentType())
179+
.setDefault(true);
180+
170181
parser.addArgument("-v", "--version")
171182
.help("print version of cypher-shell and exit")
172183
.action(new StoreTrueArgumentAction());
@@ -183,5 +194,16 @@ private static ArgumentParser setupParser()
183194
return parser;
184195
}
185196

186-
197+
private static class PositiveIntegerType implements ArgumentType<Integer> {
198+
@Override
199+
public Integer convert(ArgumentParser parser, Argument arg, String value) throws ArgumentParserException {
200+
try {
201+
int result = Integer.parseInt(value);
202+
if (result < 1) throw new NumberFormatException(value);
203+
return result;
204+
} catch (NumberFormatException nfe) {
205+
throw new ArgumentParserException("Invalid value: "+value, parser);
206+
}
207+
}
208+
}
187209
}

cypher-shell/src/main/java/org/neo4j/shell/cli/CliArgs.java

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
import java.util.Optional;
66

77
public class CliArgs {
8-
private String scheme = "bolt://";
9-
private String host = "localhost";
10-
private int port = 7687;
8+
private static final String DEFAULT_SCHEME = "bolt://";
9+
private static final String DEFAULT_HOST = "localhost";
10+
private static final int DEFAULT_PORT = 7687;
11+
static final int DEFAULT_NUM_SAMPLE_ROWS = 1000;
12+
13+
private String scheme = DEFAULT_SCHEME;
14+
private String host = DEFAULT_HOST;
15+
private int port = DEFAULT_PORT;
1116
private String username = "";
1217
private String password = "";
1318
private FailBehavior failBehavior = FailBehavior.FAIL_FAST;
@@ -18,6 +23,8 @@ public class CliArgs {
1823
private boolean nonInteractive = false;
1924
private boolean version = false;
2025
private boolean driverVersion = false;
26+
private int numSampleRows = DEFAULT_NUM_SAMPLE_ROWS;
27+
private boolean wrap = true;
2128

2229
/**
2330
* Set the scheme to the primary value, or if null, the fallback value.
@@ -106,7 +113,6 @@ public String getHost() {
106113
return host;
107114
}
108115

109-
@Nonnull
110116
public int getPort() {
111117
return port;
112118
}
@@ -167,4 +173,22 @@ public void setDriverVersion(boolean version) {
167173
public boolean isStringShell() {
168174
return cypher.isPresent();
169175
}
176+
177+
public boolean getWrap() {
178+
return wrap;
179+
}
180+
181+
public void setWrap(boolean wrap) {
182+
this.wrap = wrap;
183+
}
184+
185+
public int getNumSampleRows() {
186+
return numSampleRows;
187+
}
188+
189+
public void setNumSampleRows(Integer numSampleRows) {
190+
if (numSampleRows != null && numSampleRows > 0) {
191+
this.numSampleRows = numSampleRows;
192+
}
193+
}
170194
}

cypher-shell/src/main/java/org/neo4j/shell/log/AnsiLogger.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public class AnsiLogger implements Logger {
2323
private final PrintStream err;
2424
private final boolean debug;
2525
private Format format;
26+
private int numSampleRows;
27+
private boolean wrap = true;
2628

2729
public AnsiLogger(final boolean debug) {
2830
this(debug, Format.VERBOSE, System.out, System.err);
@@ -66,6 +68,26 @@ private static boolean isOutputInteractive() {
6668
return 1 == isatty(STDOUT_FILENO) && 1 == isatty(STDERR_FILENO);
6769
}
6870

71+
@Override
72+
public int getNumSampleRows() {
73+
return numSampleRows;
74+
}
75+
76+
@Override
77+
public void setNumSampleRows(int numSampleRows) {
78+
this.numSampleRows = numSampleRows;
79+
}
80+
81+
@Override
82+
public boolean getWrap() {
83+
return wrap;
84+
}
85+
86+
@Override
87+
public void setWrap(boolean wrap) {
88+
this.wrap = wrap;
89+
}
90+
6991
@Nonnull
7092
@Override
7193
public PrintStream getOutputStream() {

cypher-shell/src/main/java/org/neo4j/shell/log/Logger.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,12 @@ default void printIfPlain(@Nonnull String text) {
9292
printOut(text);
9393
}
9494
}
95+
96+
boolean getWrap();
97+
98+
void setWrap(boolean wrap);
99+
100+
int getNumSampleRows();
101+
102+
void setNumSampleRows(int numSampleRows);
95103
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.neo4j.shell.prettyprint;
2+
3+
/**
4+
* Prints lines.
5+
*/
6+
@FunctionalInterface
7+
public interface LinePrinter {
8+
void println( String line );
9+
}

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,7 @@
1313
import org.neo4j.shell.state.BoltResult;
1414

1515
import javax.annotation.Nonnull;
16-
import java.util.Arrays;
17-
import java.util.ArrayList;
18-
import java.util.LinkedHashMap;
19-
import java.util.List;
20-
import java.util.Map;
16+
import java.util.*;
2117
import java.util.stream.Collectors;
2218

2319
import static java.util.Arrays.asList;
@@ -26,15 +22,18 @@
2622

2723
public interface OutputFormatter {
2824

25+
enum Capablities {info, plan, result, footer, statistics}
26+
2927
String COMMA_SEPARATOR = ", ";
3028
String COLON_SEPARATOR = ": ";
3129
String COLON = ":";
3230
String SPACE = " ";
3331
String NEWLINE = System.getProperty("line.separator");
3432

35-
@Nonnull String format(@Nonnull BoltResult result);
33+
void format(@Nonnull BoltResult result, @Nonnull LinePrinter linePrinter);
3634

37-
@Nonnull default String formatValue(@Nonnull final Value value) {
35+
@Nonnull default String formatValue(final Value value) {
36+
if (value == null) return "";
3837
TypeRepresentation type = (TypeRepresentation) value.type();
3938
switch (type.constructor()) {
4039
case LIST:
@@ -101,7 +100,7 @@ default String pathAsString(@Nonnull Path path) {
101100
}
102101
}
103102

104-
return list.stream().collect(Collectors.joining());
103+
return String.join("", list);
105104
}
106105

107106
@Nonnull default String relationshipAsString(@Nonnull Relationship relationship) {
@@ -187,6 +186,7 @@ static boolean isNotBlank(String string) {
187186
return "";
188187
}
189188

189+
Set<Capablities> capabilities();
190190

191191
List<String> INFO = asList("Version", "Planner", "Runtime");
192192

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

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import javax.annotation.Nonnull;
77

8-
import static java.util.Arrays.asList;
8+
import java.util.Set;
99

1010
/**
1111
* Print the result from neo4j in a intelligible fashion.
@@ -14,17 +14,26 @@ public class PrettyPrinter {
1414
private final StatisticsCollector statisticsCollector;
1515
private final OutputFormatter outputFormatter;
1616

17-
public PrettyPrinter(@Nonnull Format format) {
17+
public PrettyPrinter(@Nonnull Format format, boolean wrap, int numSampleRows) {
1818
this.statisticsCollector = new StatisticsCollector(format);
19-
this.outputFormatter = format == Format.VERBOSE ? new TableOutputFormatter() : new SimpleOutputFormatter();
19+
this.outputFormatter = format == Format.VERBOSE ? new TableOutputFormatter(wrap, numSampleRows) : new SimpleOutputFormatter();
2020
}
2121

22-
public String format(@Nonnull final BoltResult result) {
23-
String infoOutput = outputFormatter.formatInfo(result.getSummary());
24-
String planOutput = outputFormatter.formatPlan(result.getSummary());
25-
String statistics = statisticsCollector.collect(result.getSummary());
26-
String resultOutput = outputFormatter.format(result);
27-
String footer = outputFormatter.formatFooter(result);
28-
return OutputFormatter.joinNonBlanks(OutputFormatter.NEWLINE, asList(infoOutput, planOutput, resultOutput, footer, statistics));
22+
public void format(@Nonnull final BoltResult result, LinePrinter linePrinter) {
23+
Set<OutputFormatter.Capablities> capabilities = outputFormatter.capabilities();
24+
25+
if (capabilities.contains(OutputFormatter.Capablities.result)) outputFormatter.format(result, linePrinter);
26+
27+
if (capabilities.contains(OutputFormatter.Capablities.info)) linePrinter.println(outputFormatter.formatInfo(result.getSummary()));
28+
if (capabilities.contains(OutputFormatter.Capablities.plan)) linePrinter.println(outputFormatter.formatPlan(result.getSummary()));
29+
if (capabilities.contains(OutputFormatter.Capablities.footer)) linePrinter.println(outputFormatter.formatFooter(result));
30+
if (capabilities.contains(OutputFormatter.Capablities.statistics)) linePrinter.println(statisticsCollector.collect(result.getSummary()));
31+
}
32+
33+
// Helper for testing
34+
String format(@Nonnull final BoltResult result) {
35+
StringBuilder sb = new StringBuilder();
36+
format(result, line -> {if (line!=null && !line.trim().isEmpty()) sb.append(line).append(OutputFormatter.NEWLINE);});
37+
return sb.toString();
2938
}
3039
}

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,26 @@
55
import org.neo4j.driver.v1.summary.ResultSummary;
66
import org.neo4j.shell.state.BoltResult;
77

8-
import java.util.List;
8+
import javax.annotation.Nonnull;
9+
import java.util.EnumSet;
10+
import java.util.Iterator;
911
import java.util.Map;
12+
import java.util.Set;
1013
import java.util.stream.Collectors;
1114

12-
import javax.annotation.Nonnull;
13-
1415
public class SimpleOutputFormatter implements OutputFormatter {
1516

1617
@Override
17-
@Nonnull
18-
public String format(@Nonnull final BoltResult result) {
19-
StringBuilder sb = new StringBuilder();
20-
List<Record> records = result.getRecords();
21-
if (!records.isEmpty()) {
22-
sb.append(records.get(0).keys().stream().collect(Collectors.joining(COMMA_SEPARATOR)));
23-
sb.append("\n");
24-
sb.append(records.stream().map(this::formatRecord).collect(Collectors.joining("\n")));
18+
public void format(@Nonnull BoltResult result, @Nonnull LinePrinter output) {
19+
Iterator<Record> records = result.iterate();
20+
if (records.hasNext()) {
21+
Record firstRow = records.next();
22+
output.println(String.join(COMMA_SEPARATOR, firstRow.keys()));
23+
output.println(formatRecord(firstRow));
24+
while (records.hasNext()) {
25+
output.println(formatRecord(records.next()));
26+
}
2527
}
26-
return sb.toString();
2728
}
2829

2930
@Nonnull
@@ -41,4 +42,9 @@ public String formatInfo(@Nonnull ResultSummary summary) {
4142
return info.entrySet().stream()
4243
.map( e -> String.format("%s: %s",e.getKey(),e.getValue())).collect(Collectors.joining(NEWLINE));
4344
}
45+
46+
@Override
47+
public Set<Capablities> capabilities() {
48+
return EnumSet.of(Capablities.info, Capablities.statistics, Capablities.result);
49+
}
4450
}

0 commit comments

Comments
 (0)