diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt new file mode 100644 index 0000000000..e12afe4bcd --- /dev/null +++ b/Documentation/cmd-gsql.txt @@ -0,0 +1,48 @@ +gerrit gsql +=========== + +NAME +---- +gerrit gsql - Administrative interface to active database + +SYNOPSIS +-------- +[verse] +'ssh' -p '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] diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt index 35bd7e0f3f..065ce5fa7f 100644 --- a/Documentation/cmd-index.txt +++ b/Documentation/cmd-index.txt @@ -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. diff --git a/Documentation/pgm-gsql.txt b/Documentation/pgm-gsql.txt new file mode 100644 index 0000000000..938aafd2e0 --- /dev/null +++ b/Documentation/pgm-gsql.txt @@ -0,0 +1,56 @@ +gsql +==== + +NAME +---- +gsql - Administrative interface to idle database + +SYNOPSIS +-------- +[verse] +'java' -jar gerrit.war 'gsql' -d + +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] diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt index 069ec59fa8..1ea0707560 100644 --- a/Documentation/pgm-index.txt +++ b/Documentation/pgm-index.txt @@ -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. diff --git a/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java b/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java index c55c1e5c9d..1037dd8779 100644 --- a/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java +++ b/gerrit-main/src/main/java/com/google/gerrit/main/GerritLauncher.java @@ -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"); diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java new file mode 100644 index 0000000000..b9a8ec1d1f --- /dev/null +++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Gsql.java @@ -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); + } +} diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java index e3755c0031..0a827144d8 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java @@ -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)); diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java new file mode 100644 index 0000000000..177a2e0877 --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java @@ -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(); + } + }); + } +} diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java index fd0ad50668..1319671822 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java @@ -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); } diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java new file mode 100644 index 0000000000..43944dbc6c --- /dev/null +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java @@ -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 dbFactory; + + private ReviewDb db; + private Connection connection; + private Statement statement; + + @Inject + QueryShell(final SchemaFactory 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 rows = new ArrayList(); + 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()); + } +} diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java index c0074a99cb..c32592869d 100644 --- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java +++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java @@ -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); }