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:
Shawn O. Pearce
2010-02-26 18:42:49 -08:00
parent 9f54bc41b7
commit 177ddaca30
4 changed files with 239 additions and 33 deletions

View File

@@ -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
-------- --------

View File

@@ -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;
} }

View File

@@ -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();
}
} }
}); });
} }

View File

@@ -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();