| // Copyright (C) 2009 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.google.gerrit.sshd.commands; |
| |
| import com.google.gerrit.common.Version; |
| import com.google.gerrit.reviewdb.server.ReviewDb; |
| 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 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.io.UnsupportedEncodingException; |
| 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. */ |
| public class QueryShell { |
| public interface Factory { |
| QueryShell 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<ReviewDb> dbFactory; |
| private OutputFormat outputFormat = OutputFormat.PRETTY; |
| |
| private ReviewDb db; |
| private Connection connection; |
| private Statement statement; |
| |
| @Inject |
| QueryShell(final SchemaFactory<ReviewDb> dbFactory, |
| @Assisted final InputStream in, @Assisted final OutputStream out) |
| throws UnsupportedEncodingException { |
| 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 err) { |
| out.println("fatal: Cannot open connection: " + err.getMessage()); |
| |
| } catch (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 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() { |
| 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; |
| } |
| |
| try { |
| final String[] types = {"TABLE", "VIEW"}; |
| ResultSet rs = meta.getTables(null, null, null, types); |
| try { |
| 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")); |
| } finally { |
| rs.close(); |
| } |
| } 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); |
| try { |
| 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; |
| } |
| }); |
| } finally { |
| rs.close(); |
| } |
| } catch (SQLException e) { |
| error(e); |
| return; |
| } |
| |
| try { |
| ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true); |
| try { |
| Map<String, IndexInfo> indexes = new TreeMap<String, IndexInfo>(); |
| 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); |
| } |
| } |
| } finally { |
| rs.close(); |
| } |
| } catch (SQLException e) { |
| error(e); |
| return; |
| } |
| |
| println(""); |
| } |
| |
| private void executeStatement(final String sql) { |
| final long start = System.currentTimeMillis(); |
| final boolean hasResultSet; |
| try { |
| hasResultSet = statement.execute(sql); |
| } catch (SQLException e) { |
| error(e); |
| return; |
| } |
| |
| try { |
| if (hasResultSet) { |
| final ResultSet rs = statement.getResultSet(); |
| try { |
| showResultSet(rs, false, start); |
| } finally { |
| rs.close(); |
| } |
| |
| } else { |
| final int updateCount = statement.getUpdateCount(); |
| final long ms = System.currentTimeMillis() - 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; |
| 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); |
| 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 = System.currentTimeMillis() - 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()); |
| return; |
| } |
| } |
| |
| /** |
| * 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<String[]>(); |
| 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 = System.currentTimeMillis() - 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 static abstract 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<Integer, String>(); |
| 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(); |
| } |
| } |
| } |