Implement query tool on command line and over SSH

This little query tool is primarily intended for use when running an
embedded H2 database where there really is no other way to access the
in-memory files and state.

It is however handy for quick queries against any other database.

Bug: issue 327
Change-Id: I77a96c0833026d432103a16b48ec36f315c352ec
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce 2009-11-17 20:47:04 -08:00
parent 3474d89db8
commit 7c5941ba4c
11 changed files with 644 additions and 0 deletions

View File

@ -0,0 +1,48 @@
gerrit gsql
===========
NAME
----
gerrit gsql - Administrative interface to active database
SYNOPSIS
--------
[verse]
'ssh' -p <port> <host> 'gerrit gsql'
DESCRIPTION
-----------
Provides interactive query support directly against the underlying
SQL database used by the host Gerrit server. All SQL statements
are supported, including SELECT, UPDATE, INSERT, DELETE and ALTER.
ACCESS
------
Caller must be a member of the privileged 'Administrators' group.
SCRIPTING
---------
Intended for interactive use only.
EXAMPLES
--------
To manually correct a user's SSH user name:
====
$ ssh -p 29418 review.example.com gerrit gsql
Welcome to Gerrit Code Review v2.0.25
(PostgreSQL 8.3.8)
Type '\h' for help. Type '\r' to clear the buffer.
gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;
UPDATE 1; 1 ms
gerrit> \q
Bye
$ ssh -p 29418 review.example.com gerrit flush-caches --cache sshkeys --cache accounts
====
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@ -75,6 +75,9 @@ link:cmd-create-project.html[gerrit create-project]::
link:cmd-flush-caches.html[gerrit flush-caches]::
Flush some/all server caches from memory.
link:cmd-gsql.html[gerrit gsql]::
Administrative interface to active database.
link:cmd-show-caches.html[gerrit show-caches]::
Display current cache statistics.

View File

@ -0,0 +1,56 @@
gsql
====
NAME
----
gsql - Administrative interface to idle database
SYNOPSIS
--------
[verse]
'java' -jar gerrit.war 'gsql' -d <SITE_PATH>
DESCRIPTION
-----------
Interactive query support against the configured SQL database.
All SQL statements are supported, including SELECT, UPDATE, INSERT,
DELETE and ALTER.
This command is primarily intended to access a local H2 database
which is not currently open by a Gerrit daemon. To access an open
database use link:cmd-gsql.html[gerrit gsql] over SSH.
OPTIONS
-------
-d::
\--site-path::
Location of the gerrit.config file, and all other per-site
configuration data, supporting libaries and log files.
CONTEXT
-------
This command can only be run on a server which has direct
connectivity to the metadata database, and local access to the
managed Git repositories.
EXAMPLES
--------
To manually correct a user's SSH user name:
====
$ java -jar gerrit.war gsql
Welcome to Gerrit Code Review v2.0.25
(PostgreSQL 8.3.8)
Type '\h' for help. Type '\r' to clear the buffer.
gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;
UPDATE 1; 1 ms
gerrit> \q
Bye
====
GERRIT
------
Part of link:index.html[Gerrit Code Review]

View File

@ -15,6 +15,9 @@ link:pgm-init.html[init]::
link:pgm-daemon.html[daemon]::
Gerrit HTTP, SSH network server.
link:pgm-gsql.html[gsql]::
Administrative interface to idle database.
version::
Display the release version of Gerrit Code Review.

View File

@ -56,6 +56,7 @@ public final class GerritLauncher {
System.err.println("The most commonly used commands are:");
System.err.println(" init Initialize a Gerrit installation");
System.err.println(" daemon Run the Gerrit network daemons");
System.err.println(" gsql Run the interactive query console");
System.err.println(" version Display the build version number");
System.err.println();
System.err.println(" ls List files available for cat");

View File

@ -0,0 +1,58 @@
// 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.pgm;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.server.config.FactoryModule;
import com.google.gerrit.sshd.commands.QueryShell;
import com.google.gerrit.sshd.commands.QueryShell.Factory;
import com.google.inject.Injector;
import java.io.IOException;
/** Run Gerrit's SQL query tool */
public class Gsql extends SiteProgram {
private final LifecycleManager manager = new LifecycleManager();
private Injector dbInjector;
@Override
public int run() throws Exception {
mustHaveValidSite();
dbInjector = createDbInjector();
manager.add(dbInjector);
manager.start();
RuntimeShutdown.add(new Runnable() {
public void run() {
try {
System.in.close();
} catch (IOException e) {
}
manager.stop();
}
});
shellFactory().create(System.in, System.out).run();
return 0;
}
private Factory shellFactory() {
return dbInjector.createChildInjector(new FactoryModule() {
@Override
protected void configure() {
factory(QueryShell.Factory.class);
}
}).getInstance(QueryShell.Factory.class);
}
}

View File

@ -32,6 +32,7 @@ import com.google.gerrit.sshd.args4j.AccountIdHandler;
import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
import com.google.gerrit.sshd.args4j.ProjectControlHandler;
import com.google.gerrit.sshd.commands.DefaultCommandModule;
import com.google.gerrit.sshd.commands.QueryShell;
import com.google.gerrit.util.cli.CmdLineParser;
import com.google.gerrit.util.cli.OptionHandlerFactory;
import com.google.gerrit.util.cli.OptionHandlerUtil;
@ -67,6 +68,7 @@ public class SshModule extends FactoryModule {
install(SshKeyCacheImpl.module());
bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
factory(DispatchCommand.Factory.class);
factory(QueryShell.Factory.class);
bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
.toInstance(new DispatchCommandProvider(NAME, Commands.CMD_ROOT));

View File

@ -0,0 +1,39 @@
// 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.sshd.AdminCommand;
import com.google.gerrit.sshd.BaseCommand;
import com.google.inject.Inject;
import org.apache.sshd.server.Environment;
/** Opens a query processor. */
@AdminCommand
final class AdminQueryShell extends BaseCommand {
@Inject
private QueryShell.Factory factory;
@Override
public void start(final Environment env) {
startThread(new CommandRunnable() {
@Override
public void run() throws Exception {
parseCommandLine();
factory.create(in, out).run();
}
});
}
}

View File

@ -27,6 +27,7 @@ public class MasterCommandModule extends CommandModule {
command(gerrit, "approve").to(ApproveCommand.class);
command(gerrit, "create-project").to(AdminCreateProject.class);
command(gerrit, "gsql").to(AdminQueryShell.class);
command(gerrit, "receive-pack").to(Receive.class);
command(gerrit, "replicate").to(AdminReplicate.class);
}

View File

@ -0,0 +1,432 @@
// 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.ReviewDb;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.SchemaFactory;
import com.google.gwtorm.jdbc.JdbcSchema;
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;
/** Simple interactive SQL query tool. */
public class QueryShell {
public interface Factory {
QueryShell create(@Assisted InputStream in, @Assisted OutputStream out);
}
private final BufferedReader in;
private final PrintWriter out;
private final SchemaFactory<ReviewDb> dbFactory;
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 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();
}
}
private void readEvalPrintLoop() {
final StringBuilder buffer = new StringBuilder();
boolean executed = false;
for (;;) {
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")) {
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 {
println("ERROR: '\\" + line + "' not supported");
println("");
showHelp();
}
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 {
println(" List of relations");
showResultSet(rs, "TABLE_SCHEM", "TABLE_NAME", "TABLE_TYPE");
} finally {
rs.close();
}
} catch (SQLException e) {
error(e);
}
println("");
}
private void showTable(final String tableName) {
final DatabaseMetaData meta;
try {
meta = connection.getMetaData();
} catch (SQLException e) {
error(e);
return;
}
try {
ResultSet rs = meta.getColumns(null, null, tableName, null);
try {
println(" Table " + tableName);
showResultSet(rs, "COLUMN_NAME", "TYPE_NAME");
} finally {
rs.close();
}
} catch (SQLException e) {
error(e);
}
try {
ResultSet rs = meta.getIndexInfo(null, null, tableName, false, true);
try {
if (rs.next()) {
println("");
println("Indexes on " + tableName + ":");
showResultSet(rs, "INDEX_NAME", "NON_UNIQUE", "COLUMN_NAME");
}
} finally {
rs.close();
}
} catch (SQLException e) {
error(e);
}
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 {
final int rowCount = showResultSet(rs);
final long ms = System.currentTimeMillis() - start;
println("(" + rowCount + (rowCount == 1 ? " row" : " rows") //
+ "; " + ms + " ms)");
println("");
} finally {
rs.close();
}
} else {
final int updateCount = statement.getUpdateCount();
final long ms = System.currentTimeMillis() - start;
println("UPDATE " + updateCount + "; " + ms + " ms");
}
} catch (SQLException e) {
error(e);
}
}
private int showResultSet(final ResultSet rs, String... show)
throws SQLException {
final ResultSetMetaData meta = rs.getMetaData();
final int[] columnMap;
if (show != null && 0 < show.length) {
final int colCnt = meta.getColumnCount();
columnMap = new int[show.length];
for (int colId = 0; colId < colCnt; colId++) {
final String name = meta.getColumnName(colId + 1);
for (int j = 0; j < show.length; j++) {
if (show[j].equalsIgnoreCase(name)) {
columnMap[j] = colId + 1;
break;
}
}
}
} else {
final int colCnt = meta.getColumnCount();
columnMap = new int[colCnt];
for (int colId = 0; colId < colCnt; colId++)
columnMap[colId] = colId + 1;
}
final int colCnt = columnMap.length;
final String[] names = new String[colCnt];
final int[] widths = new int[colCnt];
for (int c = 0; c < colCnt; c++) {
final int colId = columnMap[c];
names[c] = meta.getColumnLabel(colId);
widths[c] = names[c].length();
}
final List<String[]> rows = new ArrayList<String[]>();
while (rs.next()) {
final String[] row = new String[columnMap.length];
for (int c = 0; c < colCnt; c++) {
final int colId = columnMap[c];
String val = rs.getString(colId);
if (val == null) {
val = "NULL";
}
row[c] = val;
widths[c] = Math.max(widths[c], val.length());
}
rows.add(row);
}
final StringBuilder b = new StringBuilder();
for (int c = 0; c < colCnt; c++) {
final int colId = columnMap[c];
if (0 < c) {
b.append(" | ");
}
String n = names[c];
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 colId = columnMap[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");
}
return rows.size();
}
private void warning(final String msg) {
println("WARNING: " + msg);
}
private void error(final SQLException err) {
println("ERROR: " + err.getMessage());
}
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() {
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());
}
}

View File

@ -27,6 +27,7 @@ public class SlaveCommandModule extends CommandModule {
command(gerrit, "approve").to(ErrorSlaveMode.class);
command(gerrit, "create-project").to(ErrorSlaveMode.class);
command(gerrit, "gsql").to(ErrorSlaveMode.class);
command(gerrit, "receive-pack").to(ErrorSlaveMode.class);
command(gerrit, "replicate").to(ErrorSlaveMode.class);
}