Mercurial > hg > monetdb-java
diff src/main/java/nl/cwi/monetdb/client/JdbcClient.java @ 0:a5a898f6886c
Copy of MonetDB java directory changeset e6e32756ad31.
author | Sjoerd Mullender <sjoerd@acm.org> |
---|---|
date | Wed, 21 Sep 2016 09:34:48 +0200 (2016-09-21) |
parents | |
children | e67d58485172 17fbaf2635bb |
line wrap: on
line diff
new file mode 100644 --- /dev/null +++ b/src/main/java/nl/cwi/monetdb/client/JdbcClient.java @@ -0,0 +1,1193 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Copyright 1997 - July 2008 CWI, August 2008 - 2016 MonetDB B.V. + */ + +package nl.cwi.monetdb.client; + +import nl.cwi.monetdb.util.*; +import java.sql.*; +import java.io.*; +import java.util.*; +import java.util.zip.*; +import java.net.*; + +/** + * This program acts like an extended client program for MonetDB. Its + * look and feel is very much like PostgreSQL's interactive terminal + * program. Although it looks like this client is designed for MonetDB, + * it demonstrates the power of the JDBC interface since it built on top + * of JDBC only. + * + * @author Fabian Groffen, Martin van Dinther + * @version 1.3 + */ + +public final class JdbcClient { + + private static Connection con; + private static Statement stmt; + private static BufferedReader in; + private static PrintWriter out; + private static Exporter exporter; + private static DatabaseMetaData dbmd; + + public final static void main(String[] args) throws Exception { + CmdLineOpts copts = new CmdLineOpts(); + + // arguments which take exactly one argument + copts.addOption("h", "host", CmdLineOpts.CAR_ONE, "localhost", + "The hostname of the host that runs the MonetDB database. " + + "A port number can be supplied by use of a colon, i.e. " + + "-h somehost:12345."); + copts.addOption("p", "port", CmdLineOpts.CAR_ONE, "50000", + "The port number to connect to."); + // todo make it CAR_ONE_MANY + copts.addOption("f", "file", CmdLineOpts.CAR_ONE, null, + "A file name to use either for reading or writing. The " + + "file will be used for writing when dump mode is used " + + "(-D --dump). In read mode, the file can also be an URL " + + "pointing to a plain text file that is optionally gzip " + + "compressed."); + copts.addOption("u", "user", CmdLineOpts.CAR_ONE, System.getProperty("user.name"), + "The username to use when connecting to the database."); + // this one is only here for the .monetdb file parsing, it is + // removed before the command line arguments are parsed + copts.addOption(null, "password", CmdLineOpts.CAR_ONE, null, null); + copts.addOption("d", "database", CmdLineOpts.CAR_ONE, "", + "Try to connect to the given database (only makes sense " + + "if connecting to monetdbd)."); + copts.addOption("l", "language", CmdLineOpts.CAR_ONE, "sql", + "Use the given language, defaults to 'sql'."); + + // arguments which have no argument(s) + copts.addOption(null, "help", CmdLineOpts.CAR_ZERO, null, + "This help screen."); + copts.addOption(null, "version", CmdLineOpts.CAR_ZERO, null, + "Display driver version and exit."); + copts.addOption("e", "echo", CmdLineOpts.CAR_ZERO, null, + "Also outputs the contents of the input file, if any."); + copts.addOption("q", "quiet", CmdLineOpts.CAR_ZERO, null, + "Suppress printing the welcome header."); + + // arguments which have zero to many arguments + copts.addOption("D", "dump", CmdLineOpts.CAR_ZERO_MANY, null, + "Dumps the given table(s), or the complete database if " + + "none given."); + + // extended options + copts.addOption(null, "Xoutput", CmdLineOpts.CAR_ONE, null, + "The output mode when dumping. Default is sql, xml may " + + "be used for an experimental XML output."); + copts.addOption(null, "Xhash", CmdLineOpts.CAR_ONE, null, + "Use the given hash algorithm during challenge response. " + + "Supported algorithm names: SHA1, MD5, plain."); + // arguments which can have zero or one argument(s) + copts.addOption(null, "Xdebug", CmdLineOpts.CAR_ZERO_ONE, null, + "Writes a transmission log to disk for debugging purposes. " + + "If a file name is given, it is used, otherwise a file " + + "called monet<timestamp>.log is created. A given file " + + "never be overwritten; instead a unique variation of the " + + "file is used."); + copts.addOption(null, "Xbatching", CmdLineOpts.CAR_ZERO_ONE, null, + "Indicates that a batch should be used instead of direct " + + "communication with the server for each statement. If a " + + "number is given, it is used as batch size. i.e. 8000 " + + "would execute the contents on the batch after each 8000 " + + "statements read. Batching can greatly speedup the " + + "process of restoring a database dump."); + + // we store user and password in separate variables in order to + // be able to properly act on them like forgetting the password + // from the user's file if the user supplies a username on the + // command line arguments + String pass = null; + String user = null; + + // look for a file called .monetdb in the current dir or in the + // user's homedir and read its preferences + File pref = new File(".monetdb"); + if (!pref.exists()) + pref = new File(System.getProperty("user.home"), ".monetdb"); + if (pref.exists()) { + try { + copts.processFile(pref); + } catch (OptionsException e) { + System.err.println("Error in " + pref.getAbsolutePath() + ": " + e.getMessage()); + System.exit(1); + } + user = copts.getOption("user").getArgument(); + pass = copts.getOption("password").getArgument(); + } + + // process the command line arguments, remove password option + // first, and save the user we had at this point + copts.removeOption("password"); + try { + copts.processArgs(args); + } catch (OptionsException e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + // we can actually compare pointers (objects) here + if (user != copts.getOption("user").getArgument()) pass = null; + + if (copts.getOption("help").isPresent()) { + System.out.print( + "Usage java -jar jdbcclient.jar\n" + + " [-h host[:port]] [-p port] [-f file] [-u user]\n" + + " [-l language] [-d database] [-e] [-D [table]]\n" + + " [-X<opt>]\n" + + "or using long option equivalents --host --port --file --user --language\n" + + "--dump --echo --database.\n" + + "Arguments may be written directly after the option like -p50000.\n" + + "\n" + + "If no host and port are given, localhost and 50000 are assumed. An .monetdb\n" + + "file may exist in the user's home directory. This file can contain\n" + + "preferences to use each time JdbcClient is started. Options given on the\n" + + "command line override the preferences file. The .monetdb file syntax is\n" + + "<option>=<value> where option is one of the options host, port, file, mode\n" + + "debug, or password. Note that the last one is perilous and therefore not\n" + + "available as command line option.\n" + + "If no input file is given using the -f flag, an interactive session is\n" + + "started on the terminal.\n" + + "\n" + + "OPTIONS\n" + + copts.produceHelpMessage() + ); + System.exit(0); + } else if (copts.getOption("version").isPresent()) { + // We cannot use the DatabaseMetaData here, because we + // cannot get a Connection. So instead, we just get the + // values we want out of the Driver directly. + System.out.println("JDBC Driver: v" + nl.cwi.monetdb.jdbc.MonetDriver.getDriverVersion()); + System.exit(0); + } + + in = new BufferedReader(new InputStreamReader(System.in)); + out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out))); + + // whether the semi-colon at the end of a String terminates the + // query or not (default = yes => SQL) + boolean scolonterm = true; + boolean xmlMode = "xml".equals(copts.getOption("Xoutput").getArgument()); + + // we need the password from the user, fetch it with a pseudo + // password protector + if (pass == null) { + char[] tmp = System.console().readPassword("password: "); + if (tmp == null) { + System.err.println("Invalid password!"); + System.exit(1); + } + pass = String.valueOf(tmp); + } + + user = copts.getOption("user").getArgument(); + + // build the hostname + String host = copts.getOption("host").getArgument(); + if (host.indexOf(":") == -1) { + host = host + ":" + copts.getOption("port").getArgument(); + } + + // make sure the driver is loaded + Class.forName("nl.cwi.monetdb.jdbc.MonetDriver"); + + // build the extra arguments of the JDBC connect string + String attr = "?"; + CmdLineOpts.OptionContainer oc = copts.getOption("language"); + String lang = oc.getArgument(); + if (oc.isPresent()) + attr += "language=" + lang + "&"; + // set some behaviour based on the language XQuery + if (lang.equals("xquery")) { + scolonterm = false; // no ; to end a statement + if (!copts.getOption("Xoutput").isPresent()) + xmlMode = true; // the user will like xml results, most probably + } + oc = copts.getOption("Xdebug"); + if (oc.isPresent()) { + attr += "debug=true&"; + if (oc.getArgumentCount() == 1) + attr += "logfile=" + oc.getArgument() + "&"; + } + oc = copts.getOption("Xhash"); + if (oc.isPresent()) + attr += "hash=" + oc.getArgument() + "&"; + + // request a connection suitable for MonetDB from the driver + // manager note that the database specifier is only used when + // connecting to a proxy-like service, since MonetDB itself + // can't access multiple databases. + con = null; + String database = copts.getOption("database").getArgument(); + try { + con = DriverManager.getConnection( + "jdbc:monetdb://" + host + "/" + database + attr, + user, + pass + ); + SQLWarning warn = con.getWarnings(); + while (warn != null) { + System.err.println("Connection warning: " + warn.getMessage()); + warn = warn.getNextWarning(); + } + con.clearWarnings(); + } catch (SQLException e) { + System.err.println("Database connect failed: " + e.getMessage()); + System.exit(1); + } + + try { + dbmd = con.getMetaData(); + } catch (SQLException e) { + // we ignore this because it's probably because we don't use + // SQL language + dbmd = null; + } + stmt = con.createStatement(); + + // see if we will have to perform a database dump (only in SQL mode) + if ("sql".equals(lang) && copts.getOption("dump").isPresent()) { + ResultSet tbl; + + // use the given file for writing + oc = copts.getOption("file"); + if (oc.isPresent()) + out = new PrintWriter(new BufferedWriter(new FileWriter(oc.getArgument()))); + + // we only want user tables and views to be dumped, unless a specific + // table is requested + String[] types = {"TABLE", "VIEW"}; + if (copts.getOption("dump").getArgumentCount() > 0) + types = null; + // request the tables available in the current schema in the database + tbl = dbmd.getTables(null, con.getSchema(), null, types); + + List<Table> tables = new LinkedList<Table>(); + while (tbl.next()) { + tables.add(new Table( + tbl.getString("TABLE_SCHEM"), + tbl.getString("TABLE_NAME"), + tbl.getString("TABLE_TYPE"))); + } + tbl.close(); + tbl = null; + + if (xmlMode) { + exporter = new XMLExporter(out); + exporter.setProperty(XMLExporter.TYPE_NIL, XMLExporter.VALUE_XSI); + } else { + exporter = new SQLExporter(out); + // stick with SQL INSERT INTO commands for now + // in the future we might do COPY INTO's here using VALUE_COPY + exporter.setProperty(SQLExporter.TYPE_OUTPUT, SQLExporter.VALUE_INSERT); + } + exporter.useSchemas(true); + + // start SQL output + if (!xmlMode) + out.println("START TRANSACTION;\n"); + + // dump specific table(s) or not? + if (copts.getOption("dump").getArgumentCount() > 0) { // yes we do + String[] dumpers = copts.getOption("dump").getArguments(); + for (int i = 0; i < tables.size(); i++) { + Table ttmp = tables.get(i); + for (int j = 0; j < dumpers.length; j++) { + if (ttmp.getName().equalsIgnoreCase(dumpers[j].toString()) || + ttmp.getFqname().equalsIgnoreCase(dumpers[j].toString())) + { + // dump the table + doDump(out, ttmp); + } + } + } + } else { + /* this returns everything, so including SYSTEM TABLE + * constraints */ + tbl = dbmd.getImportedKeys(null, null, null); + while (tbl.next()) { + // find FK table object + Table fk = Table.findTable(tbl.getString("FKTABLE_SCHEM") + "." + tbl.getString("FKTABLE_NAME"), tables); + + // find PK table object + Table pk = Table.findTable(tbl.getString("PKTABLE_SCHEM") + "." + tbl.getString("PKTABLE_NAME"), tables); + + // this happens when a system table has referential + // constraints + if (fk == null || pk == null) + continue; + + // add PK table dependancy to FK table + fk.addDependancy(pk); + } + tbl.close(); + tbl = null; + + // search for cycles of type a -> (x ->)+ b probably not + // the most optimal way, but it works by just scanning + // every table for loops in a recursive manor + for (Table t : tables) { + Table.checkForLoop(t, new ArrayList<Table>()); + } + + // find the graph, at this point we know there are no + // cycles, thus a solution exists + for (int i = 0; i < tables.size(); i++) { + List<Table> needs = tables.get(i).requires(tables.subList(0, i + 1)); + if (needs.size() > 0) { + tables.removeAll(needs); + tables.addAll(i, needs); + + // re-evaluate this position, for there is a new + // table now + i--; + } + } + + // we now have the right order to dump tables + for (Table t : tables) { + // dump the table + doDump(out, t); + } + } + + if (!xmlMode) + out.println("COMMIT;"); + out.flush(); + + // free resources, close the statement + stmt.close(); + // close the connection with the database + con.close(); + // completed database dump + System.exit(0); + } + + if (xmlMode) { + exporter = new XMLExporter(out); + exporter.setProperty(XMLExporter.TYPE_NIL, XMLExporter.VALUE_XSI); + } else { + exporter = new SQLExporter(out); + // we want nice table formatted output + exporter.setProperty(SQLExporter.TYPE_OUTPUT, SQLExporter.VALUE_TABLE); + } + exporter.useSchemas(false); + + try { + // use the given file for reading + boolean hasFile = copts.getOption("file").isPresent(); + boolean doEcho = hasFile && copts.getOption("echo").isPresent(); + if (hasFile) { + String tmp = copts.getOption("file").getArgument(); + int batchSize = 0; + try { + in = getReader(tmp); + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + System.exit(1); + } + + // check for batch mode + oc = copts.getOption("Xbatching"); + if (oc.isPresent()) { + if (oc.getArgumentCount() == 1) { + // parse the number + try { + batchSize = Integer.parseInt(oc.getArgument()); + } catch (NumberFormatException ex) { + // complain to the user + throw new IllegalArgumentException("Illegal argument for Xbatching: " + oc.getArgument() + " is not a parseable number!"); + } + } + processBatch(batchSize); + } else { + processInteractive(true, doEcho, scolonterm, user); + } + } else { + if (!copts.getOption("quiet").isPresent()) { + // print welcome message + out.println("Welcome to the MonetDB interactive JDBC terminal!"); + if (dbmd != null) { + out.println("Database Server: " + dbmd.getDatabaseProductName() + + " v" + dbmd.getDatabaseProductVersion()); + out.println("JDBC Driver: " + dbmd.getDriverName() + + " v" + dbmd.getDriverVersion()); + } + out.println("Current Schema: " + con.getSchema()); + out.println("Type \\q to quit, \\h for a list of available commands"); + out.flush(); + } + processInteractive(false, doEcho, scolonterm, user); + } + + // free resources, close the statement + stmt.close(); + // close the connection with the database + con.close(); + // close the file (if we used a file) + in.close(); + } catch (Exception e) { + System.err.println("A fatal exception occurred: " + e.toString()); + e.printStackTrace(System.err); + // at least try to close the connection properly, since it will + // close all statements associated with it + try { + con.close(); + } catch (SQLException ex) { + // ok... nice try + } + System.exit(1); + } + } + + /** + * Tries to interpret the given String as URL or file. Returns the + * assigned BufferedReader, or throws an Exception if the given + * string couldn't be identified as a valid URL or file. + * + * @param uri URL or filename as String + * @return a BufferedReader for the uri + * @throws Exception if uri cannot be identified as a valid URL or + * file + */ + static BufferedReader getReader(String uri) throws Exception { + BufferedReader ret = null; + URL u = null; + + // Try and parse as URL first + try { + u = new URL(uri); + } catch (MalformedURLException e) { + // no URL, try as file + try { + ret = new BufferedReader(new FileReader(uri)); + } catch (FileNotFoundException fnfe) { + // the message is descriptive enough, adds "(No such file + // or directory)" itself. + throw new Exception(fnfe.getMessage()); + } + } + + if (ret == null) try { + HttpURLConnection.setFollowRedirects(true); + HttpURLConnection con = + (HttpURLConnection)u.openConnection(); + con.setRequestMethod("GET"); + String ct = con.getContentType(); + if ("application/x-gzip".equals(ct)) { + // open gzip stream + ret = new BufferedReader(new InputStreamReader( + new GZIPInputStream(con.getInputStream()))); + } else { + // text/plain otherwise just attempt to read as is + ret = new BufferedReader(new InputStreamReader( + con.getInputStream())); + } + } catch (IOException e) { + // failed to open the url + throw new Exception("No such host/file: " + e.getMessage()); + } catch (Exception e) { + // this is an exception that comes from deep ... + throw new Exception("Invalid URL: " + e.getMessage()); + } + + return ret; + } + + /** + * Starts an interactive processing loop, where output is adjusted to an + * user session. This processing loop is not suitable for bulk processing + * as in executing the contents of a file, since processing on the given + * input is done after each row that has been entered. + * + * @param hasFile a boolean indicating whether a file is used as input + * @param doEcho a boolean indicating whether to echo the given input + * @param user a String representing the username of the current user + * @throws IOException if an IO exception occurs + * @throws SQLException if a database related error occurs + */ + public static void processInteractive( + boolean hasFile, + boolean doEcho, + boolean scolonterm, + String user + ) + throws IOException, SQLException + { + // an SQL stack keeps track of ( " and ' + SQLStack stack = new SQLStack(); + // a query part is a line of an SQL query + QueryPart qp = null; + + String query = "", curLine; + boolean wasComplete = true, doProcess, lastac = false; + if (!hasFile) { + lastac = con.getAutoCommit(); + out.println("auto commit mode: " + (lastac ? "on" : "off")); + out.print(getPrompt(stack, true)); + out.flush(); + } + + // the main (interactive) process loop + for (long i = 1; true; i++) { + // Manually read a line, because we want to detect an EOF + // (ctrl-D). Doing so allows to have a terminator for query + // which is not based on a special character, as is the + // problem for XQuery + curLine = in.readLine(); + if (curLine == null) { + out.println(""); + if (!query.isEmpty()) { + try { + executeQuery(query, stmt, out, !hasFile); + } catch (SQLException e) { + out.flush(); + do { + if (hasFile) { + System.err.println("Error on line " + i + ": [" + e.getSQLState() + "] " + e.getMessage()); + } else { + System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage()); + } + // print all error messages in the chain (if any) + } while ((e = e.getNextException()) != null); + } + query = ""; + wasComplete = true; + if (!hasFile) { + boolean ac = con.getAutoCommit(); + if (ac != lastac) { + out.println("auto commit mode: " + (ac ? "on" : "off")); + lastac = ac; + } + out.print(getPrompt(stack, wasComplete)); + } + out.flush(); + // try to read again + continue; + } else { + // user did ctrl-D without something in the buffer, + // so terminate + break; + } + } + + if (doEcho) { + out.println(curLine); + out.flush(); + } + qp = scanQuery(curLine, stack, scolonterm); + if (!qp.isEmpty()) { + String command = qp.getQuery(); + doProcess = true; + if (wasComplete) { + doProcess = false; + // check for commands only when the previous row was + // complete + if (command.equals("\\q")) { + // quit + break; + } else if (command.startsWith("\\h")) { + out.println("Available commands:"); + out.println("\\q quits this program"); + out.println("\\h this help screen"); + if (dbmd != null) + out.println("\\d list available tables and views"); + out.println("\\d<obj> describes the given table or view"); + out.println("\\l<uri> executes the contents of the given file or URL"); + out.println("\\i<uri> batch executes the inserts from the given file or URL"); + } else if (dbmd != null && command.startsWith("\\d")) { + try { + String object = command.substring(2).trim().toLowerCase(); + if (scolonterm && object.endsWith(";")) + object = object.substring(0, object.length() - 1); + if (!object.isEmpty()) { + String schema; + int dot = object.indexOf("."); + if (dot != -1) { + // use provided schema + schema = object.substring(0, dot); + object = object.substring(dot + 1); + } else { + // use current schema + schema = con.getSchema(); + } + ResultSet tbl = dbmd.getTables(null, schema, null, null); + + // we have an object, see if we can find it + boolean found = false; + while (tbl.next()) { + String tableName = tbl.getString("TABLE_NAME"); + String schemaName = tbl.getString("TABLE_SCHEM"); + if ((dot == -1 && tableName.equalsIgnoreCase(object)) || + (schemaName + "." + tableName).equalsIgnoreCase(object)) + { + // we found it, describe it + exporter.dumpSchema(dbmd, + tbl.getString("TABLE_TYPE"), + tbl.getString("TABLE_CAT"), + schemaName, + tableName); + + found = true; + break; + } + } + tbl.close(); + + if (!found) + System.err.println("Unknown table or view: " + object); + } else { + String current_schema = con.getSchema(); + ResultSet tbl = dbmd.getTables(null, current_schema, null, null); + + // give us a list of all non-system tables and views (including temp ones) + while (tbl.next()) { + String tableType = tbl.getString("TABLE_TYPE"); + if (tableType != null && !tableType.startsWith("SYSTEM ")) + out.println(tableType + "\t" + + tbl.getString("TABLE_SCHEM") + "." + + tbl.getString("TABLE_NAME")); + } + tbl.close(); + } + } catch (SQLException e) { + out.flush(); + do { + System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage()); + // print all error messages in the chain (if any) + } while ((e = e.getNextException()) != null); + } + } else if (command.startsWith("\\l") || command.startsWith("\\i")) { + String object = command.substring(2).trim(); + if (scolonterm && object.endsWith(";")) + object = object.substring(0, object.length() - 1); + if (object.isEmpty()) { + System.err.println("Usage: '" + command.substring(0, 2) + "<uri>' where <uri> is a file or URL"); + } else { + // temporarily redirect input from in + BufferedReader console = in; + try { + in = getReader(object); + if (command.startsWith("\\l")) { + processInteractive(true, doEcho, scolonterm, user); + } else { + processBatch(0); + } + } catch (Exception e) { + out.flush(); + System.err.println("Error: " + e.getMessage()); + } finally { + // put back in redirection + in = console; + } + } + } else { + doProcess = true; + } + } + + if (doProcess) { + query += command + (qp.hasOpenQuote() ? "\\n" : " "); + if (qp.isComplete()) { + // strip off trailing ';' + query = query.substring(0, query.length() - 2); + // execute query + try { + executeQuery(query, stmt, out, !hasFile); + } catch (SQLException e) { + out.flush(); + do { + if (hasFile) { + System.err.println("Error on line " + i + ": [" + e.getSQLState() + "] " + e.getMessage()); + } else { + System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage()); + } + // print all error messages in the chain (if any) + } while ((e = e.getNextException()) != null); + } + query = ""; + wasComplete = true; + } else { + wasComplete = false; + } + } + } + + if (!hasFile) { + boolean ac = con.getAutoCommit(); + if (ac != lastac) { + out.println("auto commit mode: " + (ac ? "on" : "off")); + lastac = ac; + } + out.print(getPrompt(stack, wasComplete)); + } + out.flush(); + } + } + + /** + * Executes the given query and prints the result tabularised to the + * given PrintWriter stream. The result of this method is the + * default output of a query: tabular data. + * + * @param query the query to execute + * @param stmt the Statement to execute the query on + * @param out the PrintWriter to write to + * @param showTiming flag to specify if timing information nees to be printed + * @throws SQLException if a database related error occurs + */ + private static void executeQuery(String query, + Statement stmt, + PrintWriter out, + boolean showTiming) + throws SQLException + { + // warnings generated during querying + SQLWarning warn; + long startTime = (showTiming ? System.currentTimeMillis() : 0); + long finishTime = 0; + + // execute the query, let the driver decide what type it is + int aff = -1; + boolean nextRslt = stmt.execute(query, Statement.RETURN_GENERATED_KEYS); + if (!nextRslt) + aff = stmt.getUpdateCount(); + do { + if (nextRslt) { + // we have a ResultSet, print it + ResultSet rs = stmt.getResultSet(); + + exporter.dumpResultSet(rs); + if (showTiming) { + finishTime = System.currentTimeMillis(); + out.println("Elapsed Time: " + (finishTime - startTime) + " ms"); + startTime = finishTime; + } + + // if there were warnings for this result, + // show them! + warn = rs.getWarnings(); + if (warn != null) { + // force stdout to be written so the + // warning appears below it + out.flush(); + do { + System.err.println("ResultSet warning: " + + warn.getMessage()); + warn = warn.getNextWarning(); + } while (warn != null); + rs.clearWarnings(); + } + rs.close(); + } else if (aff != -1) { + String timingoutput = ""; + if (showTiming) { + finishTime = System.currentTimeMillis(); + timingoutput = ". Elapsed Time: " + (finishTime - startTime) + " ms"; + startTime = finishTime; + } + + if (aff == Statement.SUCCESS_NO_INFO) { + out.println("Operation successful" + timingoutput); + } else { + // we have an update count + // see if a key was generated + ResultSet rs = stmt.getGeneratedKeys(); + boolean hasGeneratedKeyData = rs.next(); + out.println(aff + " affected row" + (aff != 1 ? "s" : "") + + (hasGeneratedKeyData ? ", last generated key: " + rs.getString(1) : "") + + timingoutput); + rs.close(); + } + } + + out.flush(); + } while ((nextRslt = stmt.getMoreResults()) || + (aff = stmt.getUpdateCount()) != -1); + + // if there were warnings for this statement, + // and/or connection show them! + warn = stmt.getWarnings(); + while (warn != null) { + System.err.println("Statement warning: " + warn.getMessage()); + warn = warn.getNextWarning(); + } + stmt.clearWarnings(); + + warn = con.getWarnings(); + while (warn != null) { + System.err.println("Connection warning: " + warn.getMessage()); + warn = warn.getNextWarning(); + } + con.clearWarnings(); + } + + /** + * Starts a processing loop optimized for processing (large) chunks of + * continuous data, such as input from a file. Unlike in the interactive + * loop above, queries are sent only to the database if a certain batch + * amount is reached. No client side query checks are made, but everything + * is sent to the server as-is. + * + * @param batchSize the number of items to store in the batch before + * sending them to the database for execution. + * @throws IOException if an IO exception occurs. + */ + public static void processBatch(int batchSize) throws IOException { + StringBuilder query = new StringBuilder(); + String curLine; + int i = 0; + try { + // the main loop + for (i = 1; (curLine = in.readLine()) != null; i++) { + query.append(curLine); + if (curLine.endsWith(";")) { + // lousy check for end of statement, but in batch mode it + // is not very important to catch all end of statements... + stmt.addBatch(query.toString()); + query.delete(0, query.length()); + } else { + query.append('\n'); + } + if (batchSize > 0 && i % batchSize == 0) { + stmt.executeBatch(); + stmt.clearBatch(); + } + } + stmt.addBatch(query.toString()); + stmt.executeBatch(); + stmt.clearBatch(); + } catch (SQLException e) { + do { + System.err.println("Error at line " + i + ": [" + e.getSQLState() + "] " + e.getMessage()); + // print all error messages in the chain (if any) + } while ((e = e.getNextException()) != null); + } + } + + /** + * Wrapper method that decides to dump SQL or XML. In the latter case, + * this method does the XML data generation. + * + * @param out a Writer to write the data to + * @param table the table to dump + * @throws SQLException if a database related error occurs + */ + public static void doDump(PrintWriter out, Table table) throws SQLException { + String tableType = table.getType(); + + // dump CREATE definition of this table/view + exporter.dumpSchema(dbmd, tableType, null, table.getSchem(), table.getName()); + out.println(); + + // only dump data from real tables, not from views + if (tableType.contains("TABLE")) { + ResultSet rs = stmt.executeQuery("SELECT * FROM " + table.getFqnameQ()); + exporter.dumpResultSet(rs); + rs.close(); + out.println(); + } + } + + /** + * Simple helper method that generates a prompt. + * + * @param stack the current SQLStack + * @param compl whether the statement is complete + * @return a prompt which consist of "sql" plus the top of the stack + */ + private static String getPrompt(SQLStack stack, boolean compl) { + return (compl ? "sql" : "more") + + (stack.empty() ? ">" : "" + stack.peek()) + " "; + } + + /** + * Scans the given string and tries to discover if it is a complete query + * or that there needs something to be added. If a string doesn't end with + * a ; it is considered not to be complete. SQL string quotation using ' and + * SQL identifier quotation using " is taken into account when scanning a + * string this way. + * Additionally, this method removes comments from the SQL statements, + * identified by -- and removes white space where appropriate. + * + * @param query the query to parse + * @param stack query stack to work with + * @param scolonterm whether a ';' makes this query part complete + * @return a QueryPart object containing the results of this parse + */ + private static QueryPart scanQuery( + String query, + SQLStack stack, + boolean scolonterm) + { + // examine string, char for char + boolean wasInString = (stack.peek() == '\''); + boolean wasInIdentifier = (stack.peek() == '"'); + boolean escaped = false; + int len = query.length(); + for (int i = 0; i < len; i++) { + switch(query.charAt(i)) { + case '\\': + escaped = !escaped; + break; + default: + escaped = false; + break; + case '\'': + /** + * We can not be in a string if we are in an identifier. So + * If we find a ' and are not in an identifier, and not in + * a string we can safely assume we will be now in a string. + * If we are in a string already, we should stop being in a + * string if we find a quote which is not prefixed by a \, + * for that would be an escaped quote. However, a nasty + * situation can occur where the string is like 'test \\'. + * As obvious, a test for a \ in front of a ' doesn't hold + * here. Because 'test \\\'' can exist as well, we need to + * know if a quote is prefixed by an escaping slash or not. + */ + if (!escaped && stack.peek() != '"') { + if (stack.peek() != '\'') { + // although it makes no sense to escape a quote + // outside a string, it is escaped, thus not meant + // as quote for us, apparently + stack.push('\''); + } else { + stack.pop(); + } + } + // reset escaped flag + escaped = false; + break; + case '"': + if (!escaped && stack.peek() != '\'') { + if (stack.peek() != '"') { + stack.push('"'); + } else { + stack.pop(); + } + } + // reset escaped flag + escaped = false; + break; + case '-': + if (!escaped && stack.peek() != '\'' && stack.peek() != '"' && i + 1 < len && query.charAt(i + 1) == '-') { + len = i; + } + escaped = false; + break; + case '(': + if (!escaped && stack.peek() != '\'' && stack.peek() != '"') { + stack.push('('); + } + escaped = false; + break; + case ')': + if (!escaped && stack.peek() == '(') { + stack.pop(); + } + escaped = false; + break; + } + } + + int start = 0; + if (!wasInString && !wasInIdentifier && len > 0) { + // trim spaces at the start of the string + for (; start < len && Character.isWhitespace(query.charAt(start)); start++); + } + int stop = len - 1; + if (stack.peek() != '\'' && !wasInIdentifier && stop > start) { + // trim spaces at the end of the string + for (; stop >= start && Character.isWhitespace(query.charAt(stop)); stop--); + } + stop++; + + if (start == stop) { + // we have an empty string + return new QueryPart(false, null, stack.peek() == '\'' || stack.peek() == '"'); + } else if (stack.peek() == '\'' || stack.peek() == '"') { + // we have an open quote + return new QueryPart(false, query.substring(start, stop), true); + } else { + // see if the string is complete + if (scolonterm && query.charAt(stop - 1) == ';') { + return new QueryPart(true, query.substring(start, stop), false); + } else { + return new QueryPart(false, query.substring(start, stop), false); + } + } + } + + public static String dq(String in) { + return "\"" + in.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"") + "\""; + } +} + +/** + * A QueryPart is (a part of) a SQL query. In the QueryPart object information + * like the actual SQL query string, whether it has an open quote and the like + * is stored. + */ +class QueryPart { + private boolean complete; + private String query; + private boolean open; + + public QueryPart(boolean complete, String query, boolean open) { + this.complete = complete; + this.query = query; + this.open = open; + } + + public boolean isEmpty() { + return query == null; + } + + public boolean isComplete() { + return complete; + } + + public String getQuery() { + return query; + } + + public boolean hasOpenQuote() { + return open; + } +} + +/** + * An SQLStack is a simple stack that keeps track of open brackets and + * (single and double) quotes in an SQL query. + */ +class SQLStack { + StringBuilder stack = new StringBuilder(); + + public char peek() { + if (empty()) { + return '\0'; + } else { + return stack.charAt(stack.length() - 1); + } + } + + public char pop() { + char tmp = peek(); + if (tmp != '\0') { + stack.setLength(stack.length() - 1); + } + return tmp; + } + + public char push(char item) { + stack.append(item); + return item; + } + + public boolean empty() { + return stack.length() == 0; + } +} + +/** + * A Table represents an SQL table. All data required to + * generate a fully qualified name is stored, as well as dependency + * data. + */ +class Table { + final String schem; + final String name; + final String type; + final String fqname; + List<Table> needs = new ArrayList<Table>(); + + Table(String schem, String name, String type) { + this.schem = schem; + this.name = name; + this.type = type; + this.fqname = schem + "." + name; + } + + void addDependancy(Table dependsOn) throws Exception { + if (this.fqname.equals(dependsOn.fqname)) + throw new Exception("Cyclic dependancy graphs are not supported (foreign key relation references self)"); + + if (dependsOn.needs.contains(this)) + throw new Exception("Cyclic dependancy graphs are not supported (foreign key relation a->b and b->a)"); + + if (!needs.contains(dependsOn)) + needs.add(dependsOn); + } + + List<Table> requires(List<Table> existingTables) { + if (existingTables == null || existingTables.isEmpty()) + return new ArrayList<Table>(needs); + + List<Table> req = new ArrayList<Table>(); + for (Table n : needs) { + if (!existingTables.contains(n)) + req.add(n); + } + + return req; + } + + String getSchem() { + return schem; + } + + String getSchemQ() { + return JdbcClient.dq(schem); + } + + String getName() { + return name; + } + + String getNameQ() { + return JdbcClient.dq(name); + } + + String getType() { + return type; + } + + String getFqname() { + return fqname; + } + + String getFqnameQ() { + return getSchemQ() + "." + getNameQ(); + } + + public String toString() { + return fqname; + } + + + static Table findTable(String fqname, List<Table> list) { + for (Table l : list) { + if (l.fqname.equals(fqname)) + return l; + } + // not found + return null; + } + + static void checkForLoop(Table table, List<Table> parents) throws Exception { + parents.add(table); + for (int i = 0; i < table.needs.size(); i++) { + Table child = table.needs.get(i); + if (parents.contains(child)) + throw new Exception("Cyclic dependancy graphs are not supported (cycle detected for " + child.fqname + ")"); + checkForLoop(child, parents); + } + } +}