blob: 1fd180c7645c31142aa8047785994f34c4adb2ff [file] [log] [blame]
// Copyright (C) 2015 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.googlesource.gerrit.plugins.verifystatus;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.gerrit.common.TimeUtil;
import com.google.gerrit.common.Version;
import com.google.gerrit.extensions.annotations.RequiresCapability;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.server.OrmException;
import com.google.gwtorm.server.SchemaFactory;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import com.googlesource.gerrit.plugins.verifystatus.server.CiDb;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/** Simple interactive SQL query tool. */
@RequiresCapability(AccessCiDatabaseCapability.ID)
public class VerifyStatusQueryShell {
public interface Factory {
VerifyStatusQueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
}
public static enum OutputFormat {
PRETTY,
JSON,
JSON_SINGLE
}
private final BufferedReader in;
private final PrintWriter out;
private final SchemaFactory<CiDb> dbFactory;
private OutputFormat outputFormat = OutputFormat.PRETTY;
private CiDb db;
private Connection connection;
private Statement statement;
@Inject
VerifyStatusQueryShell(
SchemaFactory<CiDb> dbFactory, @Assisted InputStream in, @Assisted OutputStream out) {
this.dbFactory = dbFactory;
this.in = new BufferedReader(new InputStreamReader(in, UTF_8));
this.out = new PrintWriter(new OutputStreamWriter(out, UTF_8));
}
public void setOutputFormat(OutputFormat fmt) {
outputFormat = fmt;
}
public void run() {
try {
db = dbFactory.open();
try {
connection = ((JdbcSchema) db).getConnection();
connection.setAutoCommit(true);
statement = connection.createStatement();
try {
showBanner();
readEvalPrintLoop();
} finally {
statement.close();
statement = null;
}
} finally {
db.close();
db = null;
}
} catch (OrmException | SQLException err) {
out.println("fatal: Cannot open connection: " + err.getMessage());
} finally {
out.flush();
}
}
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 | SQLException err) {
out.println("fatal: Cannot open connection: " + err.getMessage());
} finally {
out.flush();
}
}
private void readEvalPrintLoop() {
final StringBuilder buffer = new StringBuilder();
boolean executed = false;
for (; ; ) {
if (outputFormat == OutputFormat.PRETTY) {
print(buffer.length() == 0 || executed ? "gerrit> " : " -> ");
}
String line = readLine();
if (line == null) {
return;
}
if (line.startsWith("\\")) {
// Shell command, check the various cases we recognize
//
line = line.substring(1);
if (line.equals("h") || line.equals("?")) {
showHelp();
} else if (line.equals("q")) {
if (outputFormat == OutputFormat.PRETTY) {
println("Bye");
}
return;
} else if (line.equals("r")) {
buffer.setLength(0);
executed = false;
} else if (line.equals("p")) {
println(buffer.toString());
} else if (line.equals("g")) {
if (buffer.length() > 0) {
executeStatement(buffer.toString());
executed = true;
}
} else if (line.equals("d")) {
listTables();
} else if (line.startsWith("d ")) {
showTable(line.substring(2).trim());
} else {
final String msg = "'\\" + line + "' not supported";
switch (outputFormat) {
case JSON_SINGLE:
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("");
showHelp();
break;
}
}
continue;
}
if (executed) {
buffer.setLength(0);
executed = false;
}
if (buffer.length() > 0) {
buffer.append('\n');
}
buffer.append(line);
if (buffer.length() > 0 && buffer.charAt(buffer.length() - 1) == ';') {
executeStatement(buffer.toString());
executed = true;
}
}
}
private void listTables() {
final DatabaseMetaData meta;
try {
meta = connection.getMetaData();
} catch (SQLException e) {
error(e);
return;
}
final String[] types = {"TABLE", "VIEW"};
try (ResultSet rs = meta.getTables(null, null, null, types)) {
if (outputFormat == OutputFormat.PRETTY) {
println(" List of relations");
}
showResultSet(
rs,
false,
0,
Identity.create(rs, "TABLE_SCHEM"),
Identity.create(rs, "TABLE_NAME"),
Identity.create(rs, "TABLE_TYPE"));
} catch (SQLException e) {
error(e);
}
println("");
}
private void showTable(String tableName) {
final DatabaseMetaData meta;
try {
meta = connection.getMetaData();
if (meta.storesUpperCaseIdentifiers()) {
tableName = tableName.toUpperCase();
} else if (meta.storesLowerCaseIdentifiers()) {
tableName = tableName.toLowerCase();
}
} catch (SQLException e) {
error(e);
return;
}
try (ResultSet rs = meta.getColumns(null, null, tableName, null)) {
if (!rs.next()) {
throw new SQLException("Table " + tableName + " not found");
}
if (outputFormat == OutputFormat.PRETTY) {
println(" Table " + tableName);
}
showResultSet(
rs,
true,
0,
Identity.create(rs, "COLUMN_NAME"),
new Function("TYPE") {
@Override
String apply(final ResultSet rs) throws SQLException {
String type = rs.getString("TYPE_NAME");
switch (rs.getInt("DATA_TYPE")) {
case java.sql.Types.CHAR:
case java.sql.Types.VARCHAR:
type += "(" + rs.getInt("COLUMN_SIZE") + ")";
break;
}
String def = rs.getString("COLUMN_DEF");
if (def != null && !def.isEmpty()) {
type += " DEFAULT " + def;
}
int nullable = rs.getInt("NULLABLE");
if (nullable == DatabaseMetaData.columnNoNulls) {
type += " NOT NULL";
}
return type;
}
});
} catch (SQLException e) {
error(e);
return;
}
try (ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true)) {
Map<String, IndexInfo> indexes = new TreeMap<>();
while (rs.next()) {
final String indexName = rs.getString("INDEX_NAME");
IndexInfo def = indexes.get(indexName);
if (def == null) {
def = new IndexInfo();
def.name = indexName;
indexes.put(indexName, def);
}
if (!rs.getBoolean("NON_UNIQUE")) {
def.unique = true;
}
final int pos = rs.getInt("ORDINAL_POSITION");
final String col = rs.getString("COLUMN_NAME");
String desc = rs.getString("ASC_OR_DESC");
if ("D".equals(desc)) {
desc = " DESC";
} else {
desc = "";
}
def.addColumn(pos, col + desc);
String filter = rs.getString("FILTER_CONDITION");
if (filter != null && !filter.isEmpty()) {
def.filter.append(filter);
}
}
if (outputFormat == OutputFormat.PRETTY) {
println("");
println("Indexes on " + tableName + ":");
for (IndexInfo def : indexes.values()) {
println(" " + def);
}
}
} catch (SQLException e) {
error(e);
return;
}
println("");
}
private void executeStatement(final String sql) {
final long start = TimeUtil.nowMs();
final boolean hasResultSet;
try {
hasResultSet = statement.execute(sql);
} catch (SQLException e) {
error(e);
return;
}
try {
if (hasResultSet) {
try (ResultSet rs = statement.getResultSet()) {
showResultSet(rs, false, start);
}
} else {
final int updateCount = statement.getUpdateCount();
final long ms = TimeUtil.nowMs() - start;
switch (outputFormat) {
case JSON_SINGLE:
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");
break;
}
}
} catch (SQLException e) {
error(e);
}
}
/**
* Outputs a result set to stdout.
*
* @param rs ResultSet to show.
* @param alreadyOnRow true if rs is already on the first row. false otherwise.
* @param start Timestamp in milliseconds when executing the statement started. This timestamp is
* used to compute statistics about the statement. If no statistics should be shown, set it to
* 0.
* @param show Functions to map columns
* @throws SQLException
*/
private void showResultSet(final ResultSet rs, boolean alreadyOnRow, long start, Function... show)
throws SQLException {
switch (outputFormat) {
case JSON_SINGLE:
case JSON:
showResultSetJson(rs, alreadyOnRow, start, show);
break;
case PRETTY:
default:
showResultSetPretty(rs, alreadyOnRow, start, show);
break;
}
}
/**
* Outputs a result set to stdout in Json format.
*
* @param rs ResultSet to show.
* @param alreadyOnRow true if rs is already on the first row. false otherwise.
* @param start Timestamp in milliseconds when executing the statement started. This timestamp is
* used to compute statistics about the statement. If no statistics should be shown, set it to
* 0.
* @param show Functions to map columns
* @throws SQLException
*/
private void showResultSetJson(
final ResultSet rs, boolean alreadyOnRow, long start, Function... show) throws SQLException {
JsonArray collector = new JsonArray();
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;
while (alreadyOnRow || rs.next()) {
final JsonObject row = new JsonObject();
final JsonObject cols = new JsonObject();
for (Function function : columnMap) {
String v = function.apply(rs);
if (v == null) {
continue;
}
cols.addProperty(function.name.toLowerCase(), v);
}
row.addProperty("type", "row");
row.add("columns", cols);
switch (outputFormat) {
case JSON:
println(row.toString());
break;
case JSON_SINGLE:
collector.add(row);
break;
default:
final JsonObject obj = new JsonObject();
obj.addProperty("type", "error");
obj.addProperty("message", "Unsupported Json variant");
println(obj.toString());
return;
}
alreadyOnRow = false;
rowCnt++;
}
JsonObject tail = null;
if (start != 0) {
tail = new JsonObject();
tail.addProperty("type", "query-stats");
tail.addProperty("rowCount", rowCnt);
final long ms = TimeUtil.nowMs() - start;
tail.addProperty("runTimeMilliseconds", ms);
}
switch (outputFormat) {
case JSON:
if (tail != null) {
println(tail.toString());
}
break;
case JSON_SINGLE:
if (tail != null) {
collector.add(tail);
}
println(collector.toString());
break;
default:
final JsonObject obj = new JsonObject();
obj.addProperty("type", "error");
obj.addProperty("message", "Unsupported Json variant");
println(obj.toString());
}
}
/**
* Outputs a result set to stdout in plain text format.
*
* @param rs ResultSet to show.
* @param alreadyOnRow true if rs is already on the first row. false otherwise.
* @param start Timestamp in milliseconds when executing the statement started. This timestamp is
* used to compute statistics about the statement. If no statistics should be shown, set it to
* 0.
* @param show Functions to map columns
* @throws SQLException
*/
private void showResultSetPretty(
final ResultSet rs, boolean alreadyOnRow, long start, 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);
}
}
final int colCnt = columnMap.length;
final int[] widths = new int[colCnt];
for (int c = 0; c < colCnt; c++) {
widths[c] = columnMap[c].name.length();
}
final List<String[]> rows = new ArrayList<>();
while (alreadyOnRow || rs.next()) {
final String[] row = new String[columnMap.length];
for (int c = 0; c < colCnt; c++) {
row[c] = columnMap[c].apply(rs);
if (row[c] == null) {
row[c] = "NULL";
}
widths[c] = Math.max(widths[c], row[c].length());
}
rows.add(row);
alreadyOnRow = false;
}
final StringBuilder b = new StringBuilder();
for (int c = 0; c < colCnt; c++) {
if (0 < c) {
b.append(" | ");
}
String n = columnMap[c].name;
if (widths[c] < n.length()) {
n = n.substring(0, widths[c]);
}
b.append(n);
if (c < colCnt - 1) {
for (int pad = n.length(); pad < widths[c]; pad++) {
b.append(' ');
}
}
}
println(" " + b.toString());
b.setLength(0);
for (int c = 0; c < colCnt; c++) {
if (0 < c) {
b.append("-+-");
}
for (int pad = 0; pad < widths[c]; pad++) {
b.append('-');
}
}
println(" " + b.toString());
boolean dataTruncated = false;
for (String[] row : rows) {
b.setLength(0);
b.append(' ');
for (int c = 0; c < colCnt; c++) {
final int max = widths[c];
if (0 < c) {
b.append(" | ");
}
String s = row[c];
if (1 < colCnt && max < s.length()) {
s = s.substring(0, max);
dataTruncated = true;
}
b.append(s);
if (c < colCnt - 1) {
for (int pad = s.length(); pad < max; pad++) {
b.append(' ');
}
}
}
println(b.toString());
}
if (dataTruncated) {
warning("some column data was truncated");
}
if (start != 0) {
final int rowCount = rows.size();
final long ms = TimeUtil.nowMs() - start;
println("(" + rowCount + (rowCount == 1 ? " row" : " rows") + "; " + ms + " ms)");
}
}
private void warning(final String msg) {
switch (outputFormat) {
case JSON_SINGLE:
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);
break;
}
}
private void error(final SQLException err) {
switch (outputFormat) {
case JSON_SINGLE:
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());
break;
}
}
private void print(String s) {
out.print(s);
out.flush();
}
private void println(String s) {
out.print(s);
out.print('\n');
out.flush();
}
private String readLine() {
try {
return in.readLine();
} catch (IOException e) {
return null;
}
}
private void showBanner() {
if (outputFormat == OutputFormat.PRETTY) {
println("Welcome to Gerrit Code Review " + Version.getVersion());
try {
print("(");
print(connection.getMetaData().getDatabaseProductName());
print(" ");
print(connection.getMetaData().getDatabaseProductVersion());
println(")");
} catch (SQLException err) {
error(err);
}
println("");
println("Type '\\h' for help. Type '\\r' to clear the buffer.");
println("");
}
}
private void showHelp() {
final StringBuilder help = new StringBuilder();
help.append("General\n");
help.append(" \\q quit\n");
help.append("\n");
help.append("Query Buffer\n");
help.append(" \\g execute the query buffer\n");
help.append(" \\p display the current buffer\n");
help.append(" \\r clear the query buffer\n");
help.append("\n");
help.append("Informational\n");
help.append(" \\d list all tables\n");
help.append(" \\d NAME describe table\n");
help.append("\n");
print(help.toString());
}
private abstract static class Function {
final String name;
Function(final String name) {
this.name = name;
}
abstract String apply(ResultSet rs) throws SQLException;
}
private static class Identity extends Function {
static Identity create(final ResultSet rs, final String name) throws SQLException {
return new Identity(rs.findColumn(name), name);
}
final int colId;
Identity(final int colId, final String name) {
super(name);
this.colId = colId;
}
@Override
String apply(final ResultSet rs) throws SQLException {
return rs.getString(colId);
}
}
private static class IndexInfo {
String name;
boolean unique;
final Map<Integer, String> columns = new TreeMap<>();
final StringBuilder filter = new StringBuilder();
void addColumn(int pos, String column) {
columns.put(Integer.valueOf(pos), column);
}
@Override
public String toString() {
final StringBuilder r = new StringBuilder();
r.append(name);
if (unique) {
r.append(" UNIQUE");
}
r.append(" (");
boolean first = true;
for (String c : columns.values()) {
if (!first) {
r.append(", ");
}
r.append(c);
first = false;
}
r.append(")");
if (filter.length() > 0) {
r.append(" WHERE ");
r.append(filter);
}
return r.toString();
}
}
}