Support gsql output in JSON format
I've talked to at least a few teams that are directly querying the database over gsql and scraping its output. To mirror our gerrit stream-events output gsql can now produce its records as JSON objects, making them more suitable for machine consumption. Change-Id: Ib2812b60a5d77824a48d511c50f6d8c2b23c4190 Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
@@ -8,7 +8,9 @@ gerrit gsql - Administrative interface to active database
|
|||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
--------
|
--------
|
||||||
[verse]
|
[verse]
|
||||||
'ssh' -p <port> <host> 'gerrit gsql'
|
'ssh' -p <port> <host> 'gerrit gsql' \
|
||||||
|
[\--format \{PRETTY | JSON\}] \
|
||||||
|
[\-c QUERY]
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
@@ -16,13 +18,25 @@ Provides interactive query support directly against the underlying
|
|||||||
SQL database used by the host Gerrit server. All SQL statements
|
SQL database used by the host Gerrit server. All SQL statements
|
||||||
are supported, including SELECT, UPDATE, INSERT, DELETE and ALTER.
|
are supported, including SELECT, UPDATE, INSERT, DELETE and ALTER.
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
-------
|
||||||
|
\--format::
|
||||||
|
Set the format records are output in. In PRETTY (the
|
||||||
|
default) records are displayed in a tabular output suitable
|
||||||
|
for reading by a human on a sufficiently wide terminal.
|
||||||
|
In JSON mode records are output as JSON objects using the
|
||||||
|
column names as the property names, one object per line.
|
||||||
|
|
||||||
|
-c::
|
||||||
|
Execute the single query statement supplied, and then exit.
|
||||||
|
|
||||||
ACCESS
|
ACCESS
|
||||||
------
|
------
|
||||||
Caller must be a member of the privileged 'Administrators' group.
|
Caller must be a member of the privileged 'Administrators' group.
|
||||||
|
|
||||||
SCRIPTING
|
SCRIPTING
|
||||||
---------
|
---------
|
||||||
Intended for interactive use only.
|
Intended for interactive use only, unless format is JSON.
|
||||||
|
|
||||||
EXAMPLES
|
EXAMPLES
|
||||||
--------
|
--------
|
||||||
|
@@ -24,6 +24,8 @@ import com.google.gerrit.sshd.commands.QueryShell;
|
|||||||
import com.google.gerrit.sshd.commands.QueryShell.Factory;
|
import com.google.gerrit.sshd.commands.QueryShell.Factory;
|
||||||
import com.google.inject.Injector;
|
import com.google.inject.Injector;
|
||||||
|
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
/** Run Gerrit's SQL query tool */
|
/** Run Gerrit's SQL query tool */
|
||||||
@@ -31,6 +33,12 @@ public class Gsql extends SiteProgram {
|
|||||||
private final LifecycleManager manager = new LifecycleManager();
|
private final LifecycleManager manager = new LifecycleManager();
|
||||||
private Injector dbInjector;
|
private Injector dbInjector;
|
||||||
|
|
||||||
|
@Option(name = "--format", usage = "Set output format")
|
||||||
|
private QueryShell.OutputFormat format = QueryShell.OutputFormat.PRETTY;
|
||||||
|
|
||||||
|
@Option(name = "-c", metaVar = "SQL QUERY", usage = "Query to execute")
|
||||||
|
private String query;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int run() throws Exception {
|
public int run() throws Exception {
|
||||||
mustHaveValidSite();
|
mustHaveValidSite();
|
||||||
@@ -47,7 +55,13 @@ public class Gsql extends SiteProgram {
|
|||||||
manager.stop();
|
manager.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
shellFactory().create(System.in, System.out).run();
|
final QueryShell shell = shellFactory().create(System.in, System.out);
|
||||||
|
shell.setOutputFormat(format);
|
||||||
|
if (query != null) {
|
||||||
|
shell.execute(query);
|
||||||
|
} else {
|
||||||
|
shell.run();
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ import com.google.gerrit.sshd.BaseCommand;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
import org.apache.sshd.server.Environment;
|
import org.apache.sshd.server.Environment;
|
||||||
|
import org.kohsuke.args4j.Option;
|
||||||
|
|
||||||
/** Opens a query processor. */
|
/** Opens a query processor. */
|
||||||
@AdminCommand
|
@AdminCommand
|
||||||
@@ -26,13 +27,25 @@ final class AdminQueryShell extends BaseCommand {
|
|||||||
@Inject
|
@Inject
|
||||||
private QueryShell.Factory factory;
|
private QueryShell.Factory factory;
|
||||||
|
|
||||||
|
@Option(name = "--format", usage = "Set output format")
|
||||||
|
private QueryShell.OutputFormat format = QueryShell.OutputFormat.PRETTY;
|
||||||
|
|
||||||
|
@Option(name = "-c", metaVar = "SQL QUERY", usage = "Query to execute")
|
||||||
|
private String query;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void start(final Environment env) {
|
public void start(final Environment env) {
|
||||||
startThread(new CommandRunnable() {
|
startThread(new CommandRunnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() throws Exception {
|
public void run() throws Exception {
|
||||||
parseCommandLine();
|
parseCommandLine();
|
||||||
factory.create(in, out).run();
|
final QueryShell shell = factory.create(in, out);
|
||||||
|
shell.setOutputFormat(format);
|
||||||
|
if (query != null) {
|
||||||
|
shell.execute(query);
|
||||||
|
} else {
|
||||||
|
shell.run();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -16,6 +16,7 @@ package com.google.gerrit.sshd.commands;
|
|||||||
|
|
||||||
import com.google.gerrit.common.Version;
|
import com.google.gerrit.common.Version;
|
||||||
import com.google.gerrit.reviewdb.ReviewDb;
|
import com.google.gerrit.reviewdb.ReviewDb;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
import com.google.gwtorm.client.OrmException;
|
import com.google.gwtorm.client.OrmException;
|
||||||
import com.google.gwtorm.client.SchemaFactory;
|
import com.google.gwtorm.client.SchemaFactory;
|
||||||
import com.google.gwtorm.jdbc.JdbcSchema;
|
import com.google.gwtorm.jdbc.JdbcSchema;
|
||||||
@@ -47,9 +48,14 @@ public class QueryShell {
|
|||||||
QueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
|
QueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static enum OutputFormat {
|
||||||
|
PRETTY, JSON;
|
||||||
|
}
|
||||||
|
|
||||||
private final BufferedReader in;
|
private final BufferedReader in;
|
||||||
private final PrintWriter out;
|
private final PrintWriter out;
|
||||||
private final SchemaFactory<ReviewDb> dbFactory;
|
private final SchemaFactory<ReviewDb> dbFactory;
|
||||||
|
private OutputFormat outputFormat = OutputFormat.PRETTY;
|
||||||
|
|
||||||
private ReviewDb db;
|
private ReviewDb db;
|
||||||
private Connection connection;
|
private Connection connection;
|
||||||
@@ -65,6 +71,10 @@ public class QueryShell {
|
|||||||
this.out = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
|
this.out = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setOutputFormat(OutputFormat fmt) {
|
||||||
|
outputFormat = fmt;
|
||||||
|
}
|
||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
db = dbFactory.open();
|
db = dbFactory.open();
|
||||||
@@ -94,11 +104,41 @@ public class QueryShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void execute(String query) {
|
||||||
|
try {
|
||||||
|
db = dbFactory.open();
|
||||||
|
try {
|
||||||
|
connection = ((JdbcSchema) db).getConnection();
|
||||||
|
connection.setAutoCommit(true);
|
||||||
|
|
||||||
|
statement = connection.createStatement();
|
||||||
|
try {
|
||||||
|
executeStatement(query);
|
||||||
|
} finally {
|
||||||
|
statement.close();
|
||||||
|
statement = null;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
db.close();
|
||||||
|
db = null;
|
||||||
|
}
|
||||||
|
} catch (OrmException err) {
|
||||||
|
out.println("fatal: Cannot open connection: " + err.getMessage());
|
||||||
|
|
||||||
|
} catch (SQLException err) {
|
||||||
|
out.println("fatal: Cannot open connection: " + err.getMessage());
|
||||||
|
} finally {
|
||||||
|
out.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void readEvalPrintLoop() {
|
private void readEvalPrintLoop() {
|
||||||
final StringBuilder buffer = new StringBuilder();
|
final StringBuilder buffer = new StringBuilder();
|
||||||
boolean executed = false;
|
boolean executed = false;
|
||||||
for (;;) {
|
for (;;) {
|
||||||
|
if (outputFormat == OutputFormat.PRETTY) {
|
||||||
print(buffer.length() == 0 || executed ? "gerrit> " : " -> ");
|
print(buffer.length() == 0 || executed ? "gerrit> " : " -> ");
|
||||||
|
}
|
||||||
String line = readLine();
|
String line = readLine();
|
||||||
if (line == null) {
|
if (line == null) {
|
||||||
return;
|
return;
|
||||||
@@ -112,7 +152,9 @@ public class QueryShell {
|
|||||||
showHelp();
|
showHelp();
|
||||||
|
|
||||||
} else if (line.equals("q")) {
|
} else if (line.equals("q")) {
|
||||||
|
if (outputFormat == OutputFormat.PRETTY) {
|
||||||
println("Bye");
|
println("Bye");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
|
||||||
} else if (line.equals("r")) {
|
} else if (line.equals("r")) {
|
||||||
@@ -135,9 +177,22 @@ public class QueryShell {
|
|||||||
showTable(line.substring(2).trim());
|
showTable(line.substring(2).trim());
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
println("ERROR: '\\" + line + "' not supported");
|
final String msg = "'\\" + line + "' not supported";
|
||||||
|
switch (outputFormat) {
|
||||||
|
case JSON: {
|
||||||
|
final JsonObject err = new JsonObject();
|
||||||
|
err.addProperty("type", "error");
|
||||||
|
err.addProperty("message", msg);
|
||||||
|
println(err.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PRETTY:
|
||||||
|
default:
|
||||||
|
println("ERROR: " + msg);
|
||||||
println("");
|
println("");
|
||||||
showHelp();
|
showHelp();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -171,7 +226,9 @@ public class QueryShell {
|
|||||||
final String[] types = {"TABLE", "VIEW"};
|
final String[] types = {"TABLE", "VIEW"};
|
||||||
ResultSet rs = meta.getTables(null, null, null, types);
|
ResultSet rs = meta.getTables(null, null, null, types);
|
||||||
try {
|
try {
|
||||||
|
if (outputFormat == OutputFormat.PRETTY) {
|
||||||
println(" List of relations");
|
println(" List of relations");
|
||||||
|
}
|
||||||
showResultSet(rs, false, //
|
showResultSet(rs, false, //
|
||||||
Identity.create(rs, "TABLE_SCHEM"), //
|
Identity.create(rs, "TABLE_SCHEM"), //
|
||||||
Identity.create(rs, "TABLE_NAME"), //
|
Identity.create(rs, "TABLE_NAME"), //
|
||||||
@@ -208,7 +265,9 @@ public class QueryShell {
|
|||||||
throw new SQLException("Table " + tableName + " not found");
|
throw new SQLException("Table " + tableName + " not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outputFormat == OutputFormat.PRETTY) {
|
||||||
println(" Table " + tableName);
|
println(" Table " + tableName);
|
||||||
|
}
|
||||||
showResultSet(rs, true, //
|
showResultSet(rs, true, //
|
||||||
Identity.create(rs, "COLUMN_NAME"), //
|
Identity.create(rs, "COLUMN_NAME"), //
|
||||||
new Function("TYPE") {
|
new Function("TYPE") {
|
||||||
@@ -275,11 +334,13 @@ public class QueryShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (outputFormat == OutputFormat.PRETTY) {
|
||||||
println("");
|
println("");
|
||||||
println("Indexes on " + tableName + ":");
|
println("Indexes on " + tableName + ":");
|
||||||
for (IndexInfo def : indexes.values()) {
|
for (IndexInfo def : indexes.values()) {
|
||||||
println(" " + def);
|
println(" " + def);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
rs.close();
|
rs.close();
|
||||||
}
|
}
|
||||||
@@ -307,9 +368,22 @@ public class QueryShell {
|
|||||||
try {
|
try {
|
||||||
final int rowCount = showResultSet(rs, false);
|
final int rowCount = showResultSet(rs, false);
|
||||||
final long ms = System.currentTimeMillis() - start;
|
final long ms = System.currentTimeMillis() - start;
|
||||||
|
switch (outputFormat) {
|
||||||
|
case JSON: {
|
||||||
|
final JsonObject tail = new JsonObject();
|
||||||
|
tail.addProperty("type", "query-stats");
|
||||||
|
tail.addProperty("rowCount", rowCount);
|
||||||
|
tail.addProperty("runTimeMilliseconds", ms);
|
||||||
|
println(tail.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PRETTY:
|
||||||
|
default:
|
||||||
println("(" + rowCount + (rowCount == 1 ? " row" : " rows") //
|
println("(" + rowCount + (rowCount == 1 ? " row" : " rows") //
|
||||||
+ "; " + ms + " ms)");
|
+ "; " + ms + " ms)");
|
||||||
println("");
|
break;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
rs.close();
|
rs.close();
|
||||||
}
|
}
|
||||||
@@ -317,7 +391,21 @@ public class QueryShell {
|
|||||||
} else {
|
} else {
|
||||||
final int updateCount = statement.getUpdateCount();
|
final int updateCount = statement.getUpdateCount();
|
||||||
final long ms = System.currentTimeMillis() - start;
|
final long ms = System.currentTimeMillis() - start;
|
||||||
|
switch (outputFormat) {
|
||||||
|
case JSON: {
|
||||||
|
final JsonObject tail = new JsonObject();
|
||||||
|
tail.addProperty("type", "update-stats");
|
||||||
|
tail.addProperty("rowCount", updateCount);
|
||||||
|
tail.addProperty("runTimeMilliseconds", ms);
|
||||||
|
println(tail.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PRETTY:
|
||||||
|
default:
|
||||||
println("UPDATE " + updateCount + "; " + ms + " ms");
|
println("UPDATE " + updateCount + "; " + ms + " ms");
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
error(e);
|
error(e);
|
||||||
@@ -326,6 +414,55 @@ public class QueryShell {
|
|||||||
|
|
||||||
private int showResultSet(final ResultSet rs, boolean alreadyOnRow,
|
private int showResultSet(final ResultSet rs, boolean alreadyOnRow,
|
||||||
Function... show) throws SQLException {
|
Function... show) throws SQLException {
|
||||||
|
switch (outputFormat) {
|
||||||
|
case JSON:
|
||||||
|
return showResultSetJson(rs, alreadyOnRow, show);
|
||||||
|
case PRETTY:
|
||||||
|
default:
|
||||||
|
return showResultSetPretty(rs, alreadyOnRow, show);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int showResultSetJson(final ResultSet rs, boolean alreadyOnRow,
|
||||||
|
Function... show) throws SQLException {
|
||||||
|
final ResultSetMetaData meta = rs.getMetaData();
|
||||||
|
final Function[] columnMap;
|
||||||
|
if (show != null && 0 < show.length) {
|
||||||
|
columnMap = show;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
final int colCnt = meta.getColumnCount();
|
||||||
|
columnMap = new Function[colCnt];
|
||||||
|
for (int colId = 0; colId < colCnt; colId++) {
|
||||||
|
final int p = colId + 1;
|
||||||
|
final String name = meta.getColumnLabel(p);
|
||||||
|
columnMap[colId] = new Identity(p, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int rowCnt = 0;
|
||||||
|
final int colCnt = columnMap.length;
|
||||||
|
while (alreadyOnRow || rs.next()) {
|
||||||
|
final JsonObject row = new JsonObject();
|
||||||
|
final JsonObject cols = new JsonObject();
|
||||||
|
for (int c = 0; c < colCnt; c++) {
|
||||||
|
String v = columnMap[c].apply(rs);
|
||||||
|
if (v == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cols.addProperty(columnMap[c].name.toLowerCase(), v);
|
||||||
|
}
|
||||||
|
row.addProperty("type", "row");
|
||||||
|
row.add("columns", cols);
|
||||||
|
println(row.toString());
|
||||||
|
alreadyOnRow = false;
|
||||||
|
rowCnt++;
|
||||||
|
}
|
||||||
|
return rowCnt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int showResultSetPretty(final ResultSet rs, boolean alreadyOnRow,
|
||||||
|
Function... show) throws SQLException {
|
||||||
final ResultSetMetaData meta = rs.getMetaData();
|
final ResultSetMetaData meta = rs.getMetaData();
|
||||||
|
|
||||||
final Function[] columnMap;
|
final Function[] columnMap;
|
||||||
@@ -427,11 +564,37 @@ public class QueryShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void warning(final String msg) {
|
private void warning(final String msg) {
|
||||||
|
switch (outputFormat) {
|
||||||
|
case JSON: {
|
||||||
|
final JsonObject obj = new JsonObject();
|
||||||
|
obj.addProperty("type", "warning");
|
||||||
|
obj.addProperty("message", msg);
|
||||||
|
println(obj.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PRETTY:
|
||||||
|
default:
|
||||||
println("WARNING: " + msg);
|
println("WARNING: " + msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void error(final SQLException err) {
|
private void error(final SQLException err) {
|
||||||
|
switch (outputFormat) {
|
||||||
|
case JSON: {
|
||||||
|
final JsonObject obj = new JsonObject();
|
||||||
|
obj.addProperty("type", "error");
|
||||||
|
obj.addProperty("message", err.getMessage());
|
||||||
|
println(obj.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case PRETTY:
|
||||||
|
default:
|
||||||
println("ERROR: " + err.getMessage());
|
println("ERROR: " + err.getMessage());
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void print(String s) {
|
private void print(String s) {
|
||||||
@@ -454,6 +617,7 @@ public class QueryShell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void showBanner() {
|
private void showBanner() {
|
||||||
|
if (outputFormat == OutputFormat.PRETTY) {
|
||||||
println("Welcome to Gerrit Code Review " + Version.getVersion());
|
println("Welcome to Gerrit Code Review " + Version.getVersion());
|
||||||
try {
|
try {
|
||||||
print("(");
|
print("(");
|
||||||
@@ -468,6 +632,7 @@ public class QueryShell {
|
|||||||
println("Type '\\h' for help. Type '\\r' to clear the buffer.");
|
println("Type '\\h' for help. Type '\\r' to clear the buffer.");
|
||||||
println("");
|
println("");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void showHelp() {
|
private void showHelp() {
|
||||||
final StringBuilder help = new StringBuilder();
|
final StringBuilder help = new StringBuilder();
|
||||||
|
Reference in New Issue
Block a user