comparison src/main/java/org/monetdb/client/JdbcClient.java @ 391:f523727db392

Moved Java classes from packages starting with nl.cwi.monetdb.* to package org.monetdb.* This naming complies to the Java Package Naming convention as MonetDB's main website is www.monetdb.org.
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 12 Nov 2020 22:02:01 +0100 (2020-11-12)
parents src/main/java/nl/cwi/monetdb/client/JdbcClient.java@e79718252ce6
children bf9f6b6ecf40
comparison
equal deleted inserted replaced
390:6199e0be3c6e 391:f523727db392
1 /*
2 * This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5 *
6 * Copyright 1997 - July 2008 CWI, August 2008 - 2020 MonetDB B.V.
7 */
8
9 package org.monetdb.client;
10
11 import org.monetdb.util.CmdLineOpts;
12 import org.monetdb.util.Exporter;
13 import org.monetdb.util.MDBvalidator;
14 import org.monetdb.util.OptionsException;
15 import org.monetdb.util.SQLExporter;
16 import org.monetdb.util.XMLExporter;
17
18 import java.io.BufferedReader;
19 import java.io.BufferedWriter;
20 import java.io.IOException;
21 import java.io.InputStreamReader;
22 import java.io.File;
23 import java.io.PrintWriter;
24 import java.net.HttpURLConnection;
25 import java.net.URL;
26 import java.sql.Connection;
27 import java.sql.DatabaseMetaData;
28 import java.sql.DriverManager; // this import is required as it will trigger loading the org.monetdb.jdbc.MonetDriver class
29 import java.sql.ResultSet;
30 import java.sql.ResultSetMetaData;
31 import java.sql.Statement;
32 import java.sql.SQLException;
33 import java.sql.SQLWarning;
34 import java.sql.Types;
35 import java.util.ArrayList;
36 import java.util.LinkedList;
37 import java.util.List;
38
39 /**
40 * This program acts like an extended client program for MonetDB. Its
41 * look and feel is very much like PostgreSQL's interactive terminal
42 * program. Although it looks like this client is designed for MonetDB,
43 * it demonstrates the power of the JDBC interface since it built on top
44 * of JDBC only.
45 *
46 * @author Fabian Groffen
47 * @author Martin van Dinther
48 * @version 1.6
49 */
50
51 public class JdbcClient { /* cannot (yet) be final as nl.cwi.monetdb.client.JdbcClient extends this class */
52
53 private static Connection con;
54 private static DatabaseMetaData dbmd;
55 private static Statement stmt;
56 private static BufferedReader in;
57 private static PrintWriter out;
58 private static Exporter exporter;
59
60 /**
61 * JdbcClient is a command line query tool for MonetDB, similar to mclient.
62 * It uses the JDBC API and the MonetDB JDBC driver to communicate with a
63 * MonetDB server. The MonetDB JDBC driver is included in the jdbcclient.jre8.jar
64 * for ease of use, so only 1 jar file is needed to use it.
65 *
66 * <pre>Usage java -jar jdbcclient.jre8.jar
67 * [-h host[:port]] [-p port] [-f file] [-u user]
68 * [-l language] [-d database] [-e] [-D [table]] [-X&lt;opt&gt;]
69 * | [--help] | [--version]
70 * or using long option equivalents --host --port --file --user --language
71 * --dump --echo --database.
72 * Arguments may be written directly after the option like -p50000.
73 *
74 * If no host and port are given, localhost and 50000 are assumed.
75 * An .monetdb file may exist in the user's home directory. This file can contain
76 * preferences to use each time JdbcClient is started. Options given on the
77 * command line override the preferences file. The .monetdb file syntax is
78 * &lt;option&gt;=&lt;value&gt; where option is one of the options host, port, file, mode
79 * debug, or password. Note that the last one is perilous and therefore not
80 * available as command line option.
81 * If no input file is given using the -f flag, an interactive session is
82 * started on the terminal.
83 *
84 * OPTIONS
85 * -h --host The hostname of the host that runs the MonetDB database. A port
86 * number can be supplied by use of a colon, i.e. -h somehost:12345.
87 * -p --port The port number to connect to.
88 * -f --file A file name to use either for reading or writing. The file will
89 * be used for writing when dump mode is used (-D --dump). In read
90 * mode, the file can also be an URL pointing to a plain text file
91 * that is optionally gzip compressed.
92 * -u --user The username to use when connecting to the database.
93 * -d --database Try to connect to the given database (only makes sense if
94 * connecting to monetdbd).
95 * -l --language Use the given language, defaults to 'sql'.
96 * --help This help screen.
97 * --version Display driver version and exit.
98 * -e --echo Also outputs the contents of the input file, if any.
99 * -q --quiet Suppress printing the welcome header.
100 * -D --dump Dumps the given table(s), or the complete database if none given.
101 * -Xoutput The output mode when dumping. Default is sql, xml may be used for
102 * an experimental XML output.
103 * -Xhash Use the given hash algorithm during challenge response.
104 * Supported algorithm names: SHA512, SHA384, SHA256 and SHA1.
105 * -Xdebug Writes a transmission log to disk for debugging purposes. If a
106 * file name is given, it is used, otherwise a file called
107 * monet&lt;timestamp&gt;.log is created. A given file never be
108 * overwritten; instead a unique variation of the file is used.
109 * -Xbatching Indicates that a batch should be used instead of direct
110 * communication with the server for each statement. If a number is
111 * given, it is used as batch size. i.e. 8000 would execute the
112 * contents on the batch after each 8000 statements read. Batching
113 * can greatly speedup the process of restoring a database dump.</pre>
114 *
115 * @param args optional list of startup arguments
116 * @throws Exception if uncaught exception is thrown
117 */
118 public final static void main(String[] args) throws Exception {
119 final CmdLineOpts copts = new CmdLineOpts();
120
121 // arguments which take exactly one argument
122 copts.addOption("h", "host", CmdLineOpts.CAR_ONE, "localhost",
123 "The hostname of the host that runs the MonetDB database. " +
124 "A port number can be supplied by use of a colon, i.e. " +
125 "-h somehost:12345.");
126 copts.addOption("p", "port", CmdLineOpts.CAR_ONE, "50000",
127 "The port number to connect to.");
128 // todo make it CAR_ONE_MANY
129 copts.addOption("f", "file", CmdLineOpts.CAR_ONE, null,
130 "A file name to use either for reading or writing. The " +
131 "file will be used for writing when dump mode is used " +
132 "(-D --dump). In read mode, the file can also be an URL " +
133 "pointing to a plain text file that is optionally gzip " +
134 "compressed.");
135 copts.addOption("u", "user", CmdLineOpts.CAR_ONE, System.getProperty("user.name"),
136 "The username to use when connecting to the database.");
137 // this one is only here for the .monetdb file parsing, it is
138 // removed before the command line arguments are parsed
139 copts.addOption(null, "password", CmdLineOpts.CAR_ONE, null, null);
140 copts.addOption("d", "database", CmdLineOpts.CAR_ONE, "",
141 "Try to connect to the given database (only makes sense " +
142 "if connecting to monetdbd).");
143 copts.addOption("l", "language", CmdLineOpts.CAR_ONE, "sql",
144 "Use the given language, defaults to 'sql'.");
145
146 // arguments which have no argument(s)
147 copts.addOption(null, "help", CmdLineOpts.CAR_ZERO, null,
148 "This help screen.");
149 copts.addOption(null, "version", CmdLineOpts.CAR_ZERO, null,
150 "Display driver version and exit.");
151 copts.addOption("e", "echo", CmdLineOpts.CAR_ZERO, null,
152 "Also outputs the contents of the input file, if any.");
153 copts.addOption("q", "quiet", CmdLineOpts.CAR_ZERO, null,
154 "Suppress printing the welcome header.");
155
156 // arguments which have zero to many arguments
157 copts.addOption("D", "dump", CmdLineOpts.CAR_ZERO_MANY, null,
158 "Dumps the given table(s), or the complete database if " +
159 "none given.");
160
161 // extended options
162 copts.addOption(null, "Xoutput", CmdLineOpts.CAR_ONE, null,
163 "The output mode when dumping. Default is sql, xml may " +
164 "be used for an experimental XML output.");
165 copts.addOption(null, "Xhash", CmdLineOpts.CAR_ONE, null,
166 "Use the given hash algorithm during challenge response. " +
167 "Supported algorithm names: SHA512, SHA384, SHA256 and SHA1.");
168 // arguments which can have zero or one argument(s)
169 copts.addOption(null, "Xdebug", CmdLineOpts.CAR_ZERO_ONE, null,
170 "Writes a transmission log to disk for debugging purposes. " +
171 "If a file name is given, it is used, otherwise a file " +
172 "called monet<timestamp>.log is created. A given file " +
173 "never be overwritten; instead a unique variation of the " +
174 "file is used.");
175 copts.addOption(null, "Xbatching", CmdLineOpts.CAR_ZERO_ONE, null,
176 "Indicates that a batch should be used instead of direct " +
177 "communication with the server for each statement. If a " +
178 "number is given, it is used as batch size. i.e. 8000 " +
179 "would execute the contents on the batch after each 8000 " +
180 "statements read. Batching can greatly speedup the " +
181 "process of restoring a database dump.");
182
183 // we store user and password in separate variables in order to
184 // be able to properly act on them like forgetting the password
185 // from the user's file if the user supplies a username on the
186 // command line arguments
187 String pass = null;
188 String user = null;
189
190 // look for a file called .monetdb in the current dir or in the
191 // user's homedir and read its preferences
192 File pref = new File(".monetdb");
193 if (!pref.exists())
194 pref = new File(System.getProperty("user.home"), ".monetdb");
195 if (pref.exists()) {
196 try {
197 copts.processFile(pref);
198 } catch (OptionsException e) {
199 System.err.println("Error in " + pref.getAbsolutePath() + ": " + e.getMessage());
200 System.exit(1);
201 }
202 user = copts.getOption("user").getArgument();
203 pass = copts.getOption("password").getArgument();
204 }
205
206 // process the command line arguments, remove password option
207 // first, and save the user we had at this point
208 copts.removeOption("password");
209 try {
210 copts.processArgs(args);
211 } catch (OptionsException e) {
212 System.err.println("Error: " + e.getMessage());
213 System.exit(1);
214 }
215 // we can actually compare pointers (objects) here
216 if (user != copts.getOption("user").getArgument())
217 pass = null;
218
219 if (copts.getOption("help").isPresent()) {
220 System.out.print(
221 "Usage java -jar jdbcclient.jre8.jar\n" +
222 "\t\t[-h host[:port]] [-p port] [-f file] [-u user]\n" +
223 "\t\t[-l language] [-d database] [-e] [-D [table]] [-X<opt>]\n" +
224 "\t\t| [--help] | [--version]\n" +
225 "or using long option equivalents --host --port --file --user --language\n" +
226 "--dump --echo --database.\n" +
227 "Arguments may be written directly after the option like -p50000.\n" +
228 "\n" +
229 "If no host and port are given, localhost and 50000 are assumed.\n" +
230 "An .monetdb file may exist in the user's home directory. This file can contain\n" +
231 "preferences to use each time JdbcClient is started. Options given on the\n" +
232 "command line override the preferences file. The .monetdb file syntax is\n" +
233 "<option>=<value> where option is one of the options host, port, file, mode\n" +
234 "debug, or password. Note that the last one is perilous and therefore not\n" +
235 "available as command line option.\n" +
236 "If no input file is given using the -f flag, an interactive session is\n" +
237 "started on the terminal.\n" +
238 "\n" +
239 "OPTIONS\n" +
240 copts.produceHelpMessage()
241 );
242 System.exit(0);
243 }
244
245 if (copts.getOption("version").isPresent()) {
246 // We cannot use the DatabaseMetaData here, because we
247 // cannot get a Connection. So instead, we just get the
248 // values we want out of the Driver directly.
249 System.out.println("JDBC Driver: v" + org.monetdb.jdbc.MonetDriver.getDriverVersion());
250 System.exit(0);
251 }
252
253 // whether the semi-colon at the end of a String terminates the
254 // query or not (default = yes => SQL)
255 final boolean scolonterm = true;
256 final boolean xmlMode = "xml".equals(copts.getOption("Xoutput").getArgument());
257
258 // we need the password from the user, fetch it with a pseudo
259 // password protector
260 if (pass == null) {
261 final char[] tmp = System.console().readPassword("password: ");
262 if (tmp == null) {
263 System.err.println("Invalid password!");
264 System.exit(1);
265 }
266 pass = String.valueOf(tmp);
267 }
268
269 user = copts.getOption("user").getArgument();
270
271 // build the hostname
272 String host = copts.getOption("host").getArgument();
273 if (host.indexOf(":") == -1) {
274 host = host + ":" + copts.getOption("port").getArgument();
275 }
276
277 // make sure the driver is loaded
278 // Class.forName("org.monetdb.jdbc.MonetDriver"); // not needed anymore for self registering JDBC drivers
279
280 // build the extra arguments of the JDBC connect string
281 String attr = "?";
282 CmdLineOpts.OptionContainer oc = copts.getOption("language");
283 final String lang = oc.getArgument();
284 if (oc.isPresent())
285 attr += "language=" + lang + "&";
286
287 /* Xquery is no longer functional or supported
288 // set some behaviour based on the language XQuery
289 if (lang.equals("xquery")) {
290 scolonterm = false; // no ; to end a statement
291 if (!copts.getOption("Xoutput").isPresent())
292 xmlMode = true; // the user will like xml results, most probably
293 }
294 */
295 oc = copts.getOption("Xdebug");
296 if (oc.isPresent()) {
297 attr += "debug=true&";
298 if (oc.getArgumentCount() == 1)
299 attr += "logfile=" + oc.getArgument() + "&";
300 }
301 oc = copts.getOption("Xhash");
302 if (oc.isPresent())
303 attr += "hash=" + oc.getArgument() + "&";
304
305 // request a connection suitable for MonetDB from the driver
306 // manager note that the database specifier is only used when
307 // connecting to a proxy-like service, since MonetDB itself
308 // can't access multiple databases.
309 con = null;
310 final String database = copts.getOption("database").getArgument();
311 try {
312 con = DriverManager.getConnection(
313 "jdbc:monetdb://" + host + "/" + database + attr,
314 user,
315 pass
316 );
317 SQLWarning warn = con.getWarnings();
318 while (warn != null) {
319 System.err.println("Connection warning: " + warn.getMessage());
320 warn = warn.getNextWarning();
321 }
322 con.clearWarnings();
323 } catch (SQLException e) {
324 System.err.println("Database connect failed: " + e.getMessage());
325 System.exit(1);
326 }
327
328 try {
329 dbmd = con.getMetaData();
330 } catch (SQLException e) {
331 // we ignore this because it's probably because we don't use
332 // SQL language
333 dbmd = null;
334 }
335
336 in = new BufferedReader(new InputStreamReader(System.in));
337 out = new PrintWriter(new BufferedWriter(new java.io.OutputStreamWriter(System.out)));
338
339 stmt = con.createStatement(); // is used by doDump
340
341 // see if we will have to perform a database dump (only in SQL mode)
342 if ("sql".equals(lang) && copts.getOption("dump").isPresent() && dbmd != null) {
343 // use the given file for writing
344 oc = copts.getOption("file");
345 if (oc.isPresent())
346 out = new PrintWriter(new BufferedWriter(new java.io.FileWriter(oc.getArgument())));
347
348 // we only want user tables and views to be dumped, unless a specific table is requested
349 final String[] types = {"TABLE","VIEW","MERGE TABLE","REMOTE TABLE","REPLICA TABLE","STREAM TABLE"};
350 // Future: fetch all type names using dbmd.getTableTypes() and construct String[] with all
351 // table type names excluding the SYSTEM ... ones and LOCAL TEMPORARY TABLE ones.
352
353 // request the list of tables available in the current schema in the database
354 ResultSet tbl = dbmd.getTables(null, con.getSchema(), null,
355 (copts.getOption("dump").getArgumentCount() == 0) ? types : null);
356 // fetch all tables and store them in a LinkedList of Table objects
357 final LinkedList<Table> tables = new LinkedList<Table>();
358 while (tbl.next()) {
359 tables.add(new Table(
360 tbl.getString(2), // 2 = "TABLE_SCHEM"
361 tbl.getString(3), // 3 = "TABLE_NAME"
362 tbl.getString(4))); // 4 = "TABLE_TYPE"
363 }
364 tbl.close();
365 tbl = null;
366
367 if (xmlMode) {
368 exporter = new XMLExporter(out);
369 exporter.setProperty(XMLExporter.TYPE_NIL, XMLExporter.VALUE_XSI);
370 } else {
371 exporter = new SQLExporter(out);
372 // stick with SQL INSERT INTO commands for now
373 // in the future we might do COPY INTO's here using VALUE_COPY
374 exporter.setProperty(SQLExporter.TYPE_OUTPUT, SQLExporter.VALUE_INSERT);
375 }
376 exporter.useSchemas(true);
377
378 // start SQL output
379 if (!xmlMode)
380 out.println("START TRANSACTION;\n");
381
382 // dump specific table(s) or not?
383 if (copts.getOption("dump").getArgumentCount() > 0) { // yes we do
384 final String[] dumpers = copts.getOption("dump").getArguments();
385 for (int i = 0; i < tables.size(); i++) {
386 Table ttmp = tables.get(i);
387 for (int j = 0; j < dumpers.length; j++) {
388 String dumptblnm = dumpers[j].toString();
389 if (ttmp.getName().equalsIgnoreCase(dumptblnm) ||
390 ttmp.getFqname().equalsIgnoreCase(dumptblnm))
391 {
392 // dump the table
393 doDump(out, ttmp);
394 }
395 }
396 }
397 } else {
398 /* this returns everything, so including SYSTEM TABLE constraints */
399 tbl = dbmd.getImportedKeys(null, null, null);
400 while (tbl.next()) {
401 // find FK table object 6 = "FKTABLE_SCHEM", 7 = "FKTABLE_NAME"
402 Table fk = Table.findTable(tbl.getString(6), tbl.getString(7), tables);
403
404 // find PK table object 2 = "PKTABLE_SCHEM", 3 = "PKTABLE_NAME"
405 Table pk = Table.findTable(tbl.getString(2), tbl.getString(3), tables);
406
407 // this happens when a system table has referential constraints
408 if (fk == null || pk == null)
409 continue;
410
411 // add PK table dependency to FK table
412 fk.addDependency(pk);
413 }
414 tbl.close();
415 tbl = null;
416
417 // search for cycles of type a -> (x ->)+ b probably not
418 // the most optimal way, but it works by just scanning
419 // every table for loops in a recursive manor
420 for (Table t : tables) {
421 Table.checkForLoop(t, new ArrayList<Table>());
422 }
423
424 // find the graph, at this point we know there are no
425 // cycles, thus a solution exists
426 for (int i = 0; i < tables.size(); i++) {
427 final List<Table> needs = tables.get(i).requires(tables.subList(0, i + 1));
428 if (needs.size() > 0) {
429 tables.removeAll(needs);
430 tables.addAll(i, needs);
431
432 // re-evaluate this position, for there is a new
433 // table now
434 i--;
435 }
436 }
437
438 // we now have the right order to dump tables
439 for (Table t : tables) {
440 // dump the table
441 doDump(out, t);
442 }
443 }
444
445 if (!xmlMode)
446 out.println("COMMIT;");
447 out.flush();
448
449 // free resources, close the statement
450 stmt.close();
451 // close the connection with the database
452 con.close();
453 // completed database dump
454 System.exit(0);
455 }
456
457 if (xmlMode) {
458 exporter = new XMLExporter(out);
459 exporter.setProperty(XMLExporter.TYPE_NIL, XMLExporter.VALUE_XSI);
460 } else {
461 exporter = new SQLExporter(out);
462 // we want nice table formatted output
463 exporter.setProperty(SQLExporter.TYPE_OUTPUT, SQLExporter.VALUE_TABLE);
464 }
465 exporter.useSchemas(false);
466
467 try {
468 // use the given file for reading
469 final boolean hasFile = copts.getOption("file").isPresent();
470 final boolean doEcho = hasFile && copts.getOption("echo").isPresent();
471 if (hasFile) {
472 final String tmp = copts.getOption("file").getArgument();
473 try {
474 in = getReader(tmp);
475 } catch (Exception e) {
476 System.err.println("Error: " + e.getMessage());
477 System.exit(1);
478 }
479
480 // check for batch mode
481 int batchSize = 0;
482 oc = copts.getOption("Xbatching");
483 if (oc.isPresent()) {
484 if (oc.getArgumentCount() == 1) {
485 // parse the number
486 try {
487 batchSize = Integer.parseInt(oc.getArgument());
488 } catch (NumberFormatException ex) {
489 // complain to the user
490 throw new IllegalArgumentException("Illegal argument for Xbatching: " + oc.getArgument() + " is not a parseable number!");
491 }
492 }
493 processBatch(batchSize);
494 } else {
495 processInteractive(true, doEcho, scolonterm, user);
496 }
497 } else {
498 if (!copts.getOption("quiet").isPresent()) {
499 // print welcome message
500 out.println("Welcome to the MonetDB interactive JDBC terminal!");
501 if (dbmd != null) {
502 out.println("JDBC Driver: " + dbmd.getDriverName() +
503 " v" + dbmd.getDriverVersion());
504 out.println("Database Server: " + dbmd.getDatabaseProductName() +
505 " v" + dbmd.getDatabaseProductVersion());
506 }
507 out.println("Current Schema: " + con.getSchema());
508 out.println("Type \\q to quit (you can also use: quit or exit), \\? or \\h for a list of available commands");
509 out.flush();
510 }
511 processInteractive(false, doEcho, scolonterm, user);
512 }
513
514 // free resources, close the statement
515 stmt.close();
516 // close the connection with the database
517 con.close();
518 // close the file (if we used a file)
519 in.close();
520 } catch (Exception e) {
521 System.err.println("A fatal exception occurred: " + e.toString());
522 e.printStackTrace(System.err);
523 // at least try to close the connection properly, since it will
524 // close all statements associated with it
525 try {
526 con.close();
527 } catch (SQLException ex) {
528 // ok... nice try
529 }
530 System.exit(1);
531 }
532 }
533
534 /**
535 * Tries to interpret the given String as URL or file. Returns the
536 * assigned BufferedReader, or throws an Exception if the given
537 * string couldn't be identified as a valid URL or file.
538 *
539 * @param uri URL or filename as String
540 * @return a BufferedReader for the uri
541 * @throws Exception if uri cannot be identified as a valid URL or file
542 */
543 static BufferedReader getReader(final String uri) throws Exception {
544 BufferedReader ret = null;
545 URL u = null;
546
547 // Try and parse as URL first
548 try {
549 u = new URL(uri);
550 } catch (java.net.MalformedURLException e) {
551 // no URL, try as file
552 try {
553 ret = new BufferedReader(new java.io.FileReader(uri));
554 } catch (java.io.FileNotFoundException fnfe) {
555 // the message is descriptive enough, adds "(No such file
556 // or directory)" itself.
557 throw new Exception(fnfe.getMessage());
558 }
559 }
560
561 if (ret == null) {
562 try {
563 HttpURLConnection.setFollowRedirects(true);
564 final HttpURLConnection con = (HttpURLConnection)u.openConnection();
565 con.setRequestMethod("GET");
566 final String ct = con.getContentType();
567 if ("application/x-gzip".equals(ct)) {
568 // open gzip stream
569 ret = new BufferedReader(new InputStreamReader(
570 new java.util.zip.GZIPInputStream(con.getInputStream())));
571 } else {
572 // text/plain otherwise just attempt to read as is
573 ret = new BufferedReader(new InputStreamReader(con.getInputStream()));
574 }
575 } catch (IOException e) {
576 // failed to open the url
577 throw new Exception("No such host/file: " + e.getMessage());
578 } catch (Exception e) {
579 // this is an exception that comes from deep ...
580 throw new Exception("Invalid URL: " + e.getMessage());
581 }
582 }
583
584 return ret;
585 }
586
587 /**
588 * Starts an interactive processing loop, where output is adjusted to an
589 * user session. This processing loop is not suitable for bulk processing
590 * as in executing the contents of a file, since processing on the given
591 * input is done after each row that has been entered.
592 *
593 * @param hasFile a boolean indicating whether a file is used as input
594 * @param doEcho a boolean indicating whether to echo the given input
595 * @param scolonterm whether a ';' makes this query part complete
596 * @param user a String representing the username of the current user
597 * @throws IOException if an IO exception occurs
598 * @throws SQLException if a database related error occurs
599 */
600 private static void processInteractive(
601 final boolean hasFile,
602 final boolean doEcho,
603 final boolean scolonterm,
604 final String user)
605 throws IOException, SQLException
606 {
607 // an SQL stack keeps track of ( " and '
608 final SQLStack stack = new SQLStack();
609 boolean lastac = false;
610
611 if (!hasFile) {
612 lastac = con.getAutoCommit();
613 out.println("auto commit mode: " + (lastac ? "on" : "off"));
614 out.print(getPrompt(stack, true));
615 out.flush();
616 }
617
618 String curLine;
619 String query = "";
620 boolean doProcess;
621 boolean wasComplete = true;
622
623 // the main (interactive) process loop
624 for (long i = 1; true; i++) {
625 // Manually read a line, because we want to detect an EOF
626 // (ctrl-D). Doing so allows to have a terminator for query
627 // which is not based on a special character, as is the
628 // problem for XQuery
629 curLine = in.readLine();
630 if (curLine == null) {
631 out.println("");
632 if (!query.isEmpty()) {
633 try {
634 executeQuery(query, stmt, out, !hasFile);
635 } catch (SQLException e) {
636 out.flush();
637 do {
638 if (hasFile) {
639 System.err.println("Error on line " + i + ": [" + e.getSQLState() + "] " + e.getMessage());
640 } else {
641 System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage());
642 }
643 // print all error messages in the chain (if any)
644 } while ((e = e.getNextException()) != null);
645 }
646 query = "";
647 wasComplete = true;
648 if (!hasFile) {
649 final boolean ac = con.getAutoCommit();
650 if (ac != lastac) {
651 out.println("auto commit mode: " + (ac ? "on" : "off"));
652 lastac = ac;
653 }
654 out.print(getPrompt(stack, wasComplete));
655 }
656 out.flush();
657 // try to read again
658 continue;
659 } else {
660 // user did ctrl-D without something in the buffer,
661 // so terminate
662 break;
663 }
664 }
665
666 if (doEcho) {
667 out.println(curLine);
668 out.flush();
669 }
670
671 // a query part is a line of an SQL query
672 QueryPart qp = scanQuery(curLine, stack, scolonterm);
673 if (!qp.isEmpty()) {
674 final String command = qp.getQuery();
675 doProcess = true;
676 if (wasComplete) {
677 doProcess = false;
678 // check for commands only when the previous row was complete
679 if (command.equals("\\q") || command.equals("quit") || command.equals("exit")) {
680 // quit
681 break;
682 } else if (command.equals("\\?") || command.equals("\\h")) {
683 out.println("Available commands:");
684 out.println("\\q quits this program (you can also use: quit or exit)");
685 if (dbmd != null) {
686 out.println("\\d list available user tables and views in current schema");
687 out.println("\\dS list available system tables and views in sys schema");
688 out.println("\\d <obj> describes the given table or view");
689 }
690 out.println("\\l<uri> executes the contents of the given file or URL");
691 out.println("\\i<uri> batch executes the inserts from the given file or URL");
692 out.println("\\vsci validate sql system catalog integrity");
693 // out.println("\\vsni validate sql system netcdf tables integrity"); // do not list as it depends on availability of netcdf library on server
694 // out.println("\\vsgi validate sql system geom tables integrity"); // do not list as it depends on availability of geom library on server
695 out.println("\\vsi <schema> validate integrity of data in the given schema");
696 out.println("\\vdbi validate integrity of data in all user schemas in the database");
697 out.println("\\? or \\h this help screen");
698 } else if (dbmd != null && command.startsWith("\\d")) {
699 ResultSet tbl = null;
700 try {
701 if (command.equals("\\dS")) {
702 // list available system tables and views in sys schema
703 tbl = dbmd.getTables(null, "sys", null, null);
704
705 // give us a list of all non-system tables and views (including temp ones)
706 while (tbl.next()) {
707 final String tableType = tbl.getString(4); // 4 = "TABLE_TYPE"
708 if (tableType != null && tableType.startsWith("SYSTEM ")) {
709 String tableNm = tbl.getString(3); // 3 = "TABLE_NAME"
710 if (tableNm.contains(" ") || tableNm.contains("\t"))
711 tableNm = dq(tableNm);
712 out.println(tableType + "\t" +
713 tbl.getString(2) + "." + // 2 = "TABLE_SCHEM"
714 tableNm);
715 }
716 }
717 } else {
718 String object = command.substring(2).trim();
719 if (scolonterm && object.endsWith(";"))
720 object = object.substring(0, object.length() - 1);
721 if (object.isEmpty()) {
722 // list available user tables and views in current schema
723 tbl = dbmd.getTables(null, con.getSchema(), null, null);
724
725 // give us a list of all non-system tables and views (including temp ones)
726 while (tbl.next()) {
727 final String tableType = tbl.getString(4); // 4 = "TABLE_TYPE"
728 if (tableType != null && !tableType.startsWith("SYSTEM ")) {
729 String tableNm = tbl.getString(3); // 3 = "TABLE_NAME"
730 if (tableNm.contains(" ") || tableNm.contains("\t"))
731 tableNm = dq(tableNm);
732 out.println(tableType + "\t" +
733 tbl.getString(2) + "." + // 2 = "TABLE_SCHEM"
734 tableNm);
735 }
736 }
737 } else {
738 // describes the given table or view
739 String schema;
740 String obj_nm = object;
741 int len;
742 boolean found = false;
743 final int dot = object.indexOf(".");
744 if (dot > 0) {
745 // use specified schema
746 schema = object.substring(0, dot);
747 obj_nm = object.substring(dot + 1);
748 // remove potential surrounding double quotes around schema name
749 len = schema.length();
750 if (len > 2 && schema.charAt(0) == '"' && schema.charAt(len -1) == '"')
751 schema = schema.substring(1, len -1);
752 } else {
753 // use current schema
754 schema = con.getSchema();
755 }
756 // remove potential surrounding double quotes around table or view name
757 len = obj_nm.length();
758 if (len > 2 && obj_nm.charAt(0) == '"' && obj_nm.charAt(len -1) == '"')
759 obj_nm = obj_nm.substring(1, len -1);
760
761 // System.err.println("calling dbmd.getTables(" + schema + ", " + obj_nm + ")");
762 tbl = dbmd.getTables(null, schema, obj_nm, null);
763 while (tbl.next() && !found) {
764 final String schemaName = tbl.getString(2); // 2 = "TABLE_SCHEM"
765 final String tableName = tbl.getString(3); // 3 = "TABLE_NAME"
766 if (obj_nm.equals(tableName) && schema.equals(schemaName)) {
767 // we found it, describe it
768 exporter.dumpSchema(dbmd,
769 tbl.getString(4), // 4 = "TABLE_TYPE"
770 schemaName,
771 tableName);
772
773 found = true;
774 break;
775 }
776 }
777 if (!found)
778 System.err.println("No match found for table or view: " + object);
779 }
780 }
781 } catch (SQLException e) {
782 out.flush();
783 do {
784 System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage());
785 // print all error messages in the chain (if any)
786 } while ((e = e.getNextException()) != null);
787 } finally {
788 if (tbl != null)
789 tbl.close();
790 }
791 } else if (command.equals("\\vsci")) {
792 MDBvalidator.validateSqlCatalogIntegrity(con);
793 } else if (command.equals("\\vsni")) {
794 MDBvalidator.validateSqlNetcdfTablesIntegrity(con);
795 } else if (command.equals("\\vsgi")) {
796 MDBvalidator.validateSqlGeomTablesIntegrity(con);
797 } else if (command.startsWith("\\vsi ")) {
798 String schema_arg = command.substring(5);
799 MDBvalidator.validateSchemaIntegrity(con, schema_arg);
800 } else if (command.equals("\\vdbi")) {
801 MDBvalidator.validateDBIntegrity(con);
802 } else if (command.startsWith("\\l") || command.startsWith("\\i")) {
803 String object = command.substring(2).trim();
804 if (scolonterm && object.endsWith(";"))
805 object = object.substring(0, object.length() - 1);
806 if (object.isEmpty()) {
807 System.err.println("Usage: '" + command.substring(0, 2) + "<uri>' where <uri> is a file or URL");
808 } else {
809 // temporarily redirect input from in
810 final BufferedReader console = in;
811 try {
812 in = getReader(object);
813 if (command.startsWith("\\l")) {
814 processInteractive(true, doEcho, scolonterm, user);
815 } else {
816 processBatch(0);
817 }
818 } catch (Exception e) {
819 out.flush();
820 System.err.println("Error: " + e.getMessage());
821 } finally {
822 // put back in redirection
823 in = console;
824 }
825 }
826 } else {
827 doProcess = true;
828 }
829 }
830
831 if (doProcess) {
832 query += command + (qp.hasOpenQuote() ? "\\n" : " ");
833 if (qp.isComplete()) {
834 // strip off trailing ';'
835 query = query.substring(0, query.length() - 2);
836 // execute query
837 try {
838 executeQuery(query, stmt, out, !hasFile);
839 } catch (SQLException e) {
840 out.flush();
841 final String startmsg = (hasFile ? ("Error on line " + i + ": [") : "Error [");
842 do {
843 System.err.println(startmsg + e.getSQLState() + "] " + e.getMessage());
844 // print all error messages in the chain (if any)
845 } while ((e = e.getNextException()) != null);
846 }
847 query = "";
848 wasComplete = true;
849 } else {
850 wasComplete = false;
851 }
852 }
853 }
854
855 if (!hasFile) {
856 final boolean ac = con.getAutoCommit();
857 if (ac != lastac) {
858 out.println("auto commit mode: " + (ac ? "on" : "off"));
859 lastac = ac;
860 }
861 out.print(getPrompt(stack, wasComplete));
862 }
863 out.flush();
864 }
865 }
866
867 /**
868 * Executes the given query and prints the result tabularised to the
869 * given PrintWriter stream. The result of this method is the
870 * default output of a query: tabular data.
871 *
872 * @param query the query to execute
873 * @param stmt the Statement to execute the query on
874 * @param out the PrintWriter to write to
875 * @param showTiming flag to specify if timing information nees to be printed
876 * @throws SQLException if a database related error occurs
877 */
878 private static void executeQuery(final String query,
879 final Statement stmt,
880 final PrintWriter out,
881 final boolean showTiming)
882 throws SQLException
883 {
884 // warnings generated during querying
885 SQLWarning warn;
886 long startTime = (showTiming ? System.currentTimeMillis() : 0);
887 long finishTime = 0;
888
889 // execute the query, let the driver decide what type it is
890 int aff = -1;
891 boolean nextRslt = stmt.execute(query, Statement.RETURN_GENERATED_KEYS);
892 if (!nextRslt)
893 aff = stmt.getUpdateCount();
894 do {
895 if (nextRslt) {
896 // we have a ResultSet, print it
897 final ResultSet rs = stmt.getResultSet();
898
899 exporter.dumpResultSet(rs);
900 if (showTiming) {
901 finishTime = System.currentTimeMillis();
902 out.println("Elapsed Time: " + (finishTime - startTime) + " ms");
903 startTime = finishTime;
904 }
905
906 // if there were warnings for this result,
907 // show them!
908 warn = rs.getWarnings();
909 if (warn != null) {
910 // force stdout to be written so the
911 // warning appears below it
912 out.flush();
913 do {
914 System.err.println("ResultSet warning: " +
915 warn.getMessage());
916 warn = warn.getNextWarning();
917 } while (warn != null);
918 rs.clearWarnings();
919 }
920 rs.close();
921 } else if (aff != -1) {
922 String timingoutput = "";
923 if (showTiming) {
924 finishTime = System.currentTimeMillis();
925 timingoutput = ". Elapsed Time: " + (finishTime - startTime) + " ms";
926 startTime = finishTime;
927 }
928
929 if (aff == Statement.SUCCESS_NO_INFO) {
930 out.println("Operation successful" + timingoutput);
931 } else {
932 // we have an update count
933 // see if a key was generated
934 final ResultSet rs = stmt.getGeneratedKeys();
935 final boolean hasGeneratedKeyData = rs.next();
936 out.println(aff + " affected row" + (aff != 1 ? "s" : "") +
937 (hasGeneratedKeyData ? ", last generated key: " + rs.getString(1) : "") +
938 timingoutput);
939 rs.close();
940 }
941 }
942
943 out.flush();
944 } while ((nextRslt = stmt.getMoreResults()) ||
945 (aff = stmt.getUpdateCount()) != -1);
946
947 // if there were warnings for this statement,
948 // and/or connection show them!
949 warn = stmt.getWarnings();
950 while (warn != null) {
951 System.err.println("Statement warning: " + warn.getMessage());
952 warn = warn.getNextWarning();
953 }
954 stmt.clearWarnings();
955
956 warn = con.getWarnings();
957 while (warn != null) {
958 // suppress warning when issueing a "set schema xyz;" command
959 // if ( !(warn.getMessage()).equals("Server enabled auto commit mode while local state already was auto commit.") )
960 System.err.println("Connection warning: " + warn.getMessage());
961 warn = warn.getNextWarning();
962 }
963 con.clearWarnings();
964 }
965
966 /**
967 * Starts a processing loop optimized for processing (large) chunks of
968 * continuous data, such as input from a file. Unlike in the interactive
969 * loop above, queries are sent only to the database if a certain batch
970 * amount is reached. No client side query checks are made, but everything
971 * is sent to the server as-is.
972 *
973 * @param batchSize the number of items to store in the batch before
974 * sending them to the database for execution.
975 * @throws IOException if an IO exception occurs.
976 */
977 private static void processBatch(final int batchSize) throws IOException {
978 final StringBuilder query = new StringBuilder(2048);
979 int i = 0;
980 try {
981 String curLine;
982 // the main loop
983 for (i = 1; (curLine = in.readLine()) != null; i++) {
984 query.append(curLine);
985 if (curLine.endsWith(";")) {
986 // lousy check for end of statement, but in batch mode it
987 // is not very important to catch all end of statements...
988 stmt.addBatch(query.toString());
989 query.setLength(0); // clear the buffer
990 } else {
991 query.append('\n');
992 }
993 if (batchSize > 0 && i % batchSize == 0) {
994 stmt.executeBatch();
995 // stmt.clearBatch(); // this is no longer needed after call executeBatch(), see https://www.monetdb.org/bugzilla/show_bug.cgi?id=6953
996 }
997 }
998 stmt.addBatch(query.toString());
999 stmt.executeBatch();
1000 // stmt.clearBatch(); // this is no longer needed after call executeBatch(), see https://www.monetdb.org/bugzilla/show_bug.cgi?id=6953
1001 } catch (SQLException e) {
1002 do {
1003 System.err.println("Error at line " + i + ": [" + e.getSQLState() + "] " + e.getMessage());
1004 // print all error messages in the chain (if any)
1005 } while ((e = e.getNextException()) != null);
1006 }
1007 }
1008
1009 /**
1010 * Wrapper method that decides to dump SQL or XML. In the latter case,
1011 * this method does the XML data generation.
1012 *
1013 * @param out a Writer to write the data to
1014 * @param table the table to dump
1015 * @throws SQLException if a database related error occurs
1016 */
1017 private static void doDump(final PrintWriter out, final Table table) throws SQLException {
1018 final String tableType = table.getType();
1019
1020 // dump CREATE definition of this table/view
1021 exporter.dumpSchema(dbmd, tableType, table.getSchem(), table.getName());
1022 out.println();
1023
1024 // only dump data from real tables, not from views / MERGE / REMOTE / REPLICA tables
1025 if (tableType.contains("TABLE")
1026 && !tableType.equals("MERGE TABLE")
1027 && !tableType.equals("REMOTE TABLE")
1028 && !tableType.equals("REPLICA TABLE")) {
1029 final ResultSet rs = stmt.executeQuery("SELECT * FROM " + table.getFqnameQ());
1030 if (rs != null) {
1031 exporter.dumpResultSet(rs);
1032 rs.close();
1033 out.println();
1034 }
1035 }
1036 }
1037
1038 /**
1039 * Simple helper method that generates a prompt.
1040 *
1041 * @param stack the current SQLStack
1042 * @param compl whether the statement is complete
1043 * @return a prompt which consist of "sql" plus the top of the stack
1044 */
1045 private static String getPrompt(final SQLStack stack, final boolean compl) {
1046 return (compl ? "sql" : "more") + (stack.empty() ? ">" : stack.peek()) + " ";
1047 }
1048
1049 /**
1050 * Scans the given string and tries to discover if it is a complete query
1051 * or that there needs something to be added. If a string doesn't end with
1052 * a ; it is considered not to be complete. SQL string quotation using ' and
1053 * SQL identifier quotation using " is taken into account when scanning a
1054 * string this way.
1055 * Additionally, this method removes comments from the SQL statements,
1056 * identified by -- and removes white space where appropriate.
1057 *
1058 * @param query the query to parse
1059 * @param stack query stack to work with
1060 * @param scolonterm whether a ';' makes this query part complete
1061 * @return a QueryPart object containing the results of this parse
1062 */
1063 private static QueryPart scanQuery(
1064 final String query,
1065 final SQLStack stack,
1066 final boolean scolonterm)
1067 {
1068 // examine string, char for char
1069 boolean wasInString = (stack.peek() == '\'');
1070 boolean wasInIdentifier = (stack.peek() == '"');
1071 boolean escaped = false;
1072 int len = query.length();
1073 for (int i = 0; i < len; i++) {
1074 switch(query.charAt(i)) {
1075 case '\\':
1076 escaped = !escaped;
1077 break;
1078 default:
1079 escaped = false;
1080 break;
1081 case '\'':
1082 /**
1083 * We can not be in a string if we are in an identifier. So
1084 * If we find a ' and are not in an identifier, and not in
1085 * a string we can safely assume we will be now in a string.
1086 * If we are in a string already, we should stop being in a
1087 * string if we find a quote which is not prefixed by a \,
1088 * for that would be an escaped quote. However, a nasty
1089 * situation can occur where the string is like 'test \\'.
1090 * As obvious, a test for a \ in front of a ' doesn't hold
1091 * here. Because 'test \\\'' can exist as well, we need to
1092 * know if a quote is prefixed by an escaping slash or not.
1093 */
1094 if (!escaped && stack.peek() != '"') {
1095 if (stack.peek() != '\'') {
1096 // although it makes no sense to escape a quote
1097 // outside a string, it is escaped, thus not meant
1098 // as quote for us, apparently
1099 stack.push('\'');
1100 } else {
1101 stack.pop();
1102 }
1103 }
1104 // reset escaped flag
1105 escaped = false;
1106 break;
1107 case '"':
1108 if (!escaped && stack.peek() != '\'') {
1109 if (stack.peek() != '"') {
1110 stack.push('"');
1111 } else {
1112 stack.pop();
1113 }
1114 }
1115 // reset escaped flag
1116 escaped = false;
1117 break;
1118 case '-':
1119 if (!escaped && stack.peek() != '\'' && stack.peek() != '"' && i + 1 < len && query.charAt(i + 1) == '-') {
1120 len = i;
1121 }
1122 escaped = false;
1123 break;
1124 case '(':
1125 if (!escaped && stack.peek() != '\'' && stack.peek() != '"') {
1126 stack.push('(');
1127 }
1128 escaped = false;
1129 break;
1130 case ')':
1131 if (!escaped && stack.peek() == '(') {
1132 stack.pop();
1133 }
1134 escaped = false;
1135 break;
1136 }
1137 }
1138
1139 int start = 0;
1140 if (!wasInString && !wasInIdentifier && len > 0) {
1141 // trim spaces at the start of the string
1142 for (; start < len && Character.isWhitespace(query.charAt(start)); start++);
1143 }
1144 int stop = len - 1;
1145 if (stack.peek() != '\'' && !wasInIdentifier && stop > start) {
1146 // trim spaces at the end of the string
1147 for (; stop >= start && Character.isWhitespace(query.charAt(stop)); stop--);
1148 }
1149 stop++;
1150
1151 if (start == stop) {
1152 // we have an empty string
1153 return new QueryPart(false, null, stack.peek() == '\'' || stack.peek() == '"');
1154 } else if (stack.peek() == '\'' || stack.peek() == '"') {
1155 // we have an open quote
1156 return new QueryPart(false, query.substring(start, stop), true);
1157 } else {
1158 // see if the string is complete
1159 if (scolonterm && query.charAt(stop - 1) == ';') {
1160 return new QueryPart(true, query.substring(start, stop), false);
1161 } else {
1162 return new QueryPart(false, query.substring(start, stop), false);
1163 }
1164 }
1165 }
1166
1167 static final String dq(final String in) {
1168 return "\"" + in.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"") + "\"";
1169 }
1170 }
1171
1172 /**
1173 * A QueryPart is (a part of) a SQL query. In the QueryPart object information
1174 * like the actual SQL query string, whether it has an open quote and the like
1175 * is stored.
1176 */
1177 final class QueryPart {
1178 private final boolean complete;
1179 private final String query;
1180 private final boolean open;
1181
1182 QueryPart(final boolean complete, final String query, final boolean open) {
1183 this.complete = complete;
1184 this.query = query;
1185 this.open = open;
1186 }
1187
1188 boolean isEmpty() {
1189 return query == null;
1190 }
1191
1192 boolean isComplete() {
1193 return complete;
1194 }
1195
1196 String getQuery() {
1197 return query;
1198 }
1199
1200 boolean hasOpenQuote() {
1201 return open;
1202 }
1203 }
1204
1205 /**
1206 * An SQLStack is a simple stack that keeps track of open brackets and
1207 * (single and double) quotes in an SQL query.
1208 */
1209 final class SQLStack {
1210 final StringBuilder stack = new StringBuilder();
1211
1212 char peek() {
1213 if (empty()) {
1214 return '\0';
1215 } else {
1216 return stack.charAt(stack.length() - 1);
1217 }
1218 }
1219
1220 char pop() {
1221 final char tmp = peek();
1222 if (tmp != '\0') {
1223 stack.setLength(stack.length() - 1);
1224 }
1225 return tmp;
1226 }
1227
1228 char push(char item) {
1229 stack.append(item);
1230 return item;
1231 }
1232
1233 boolean empty() {
1234 return stack.length() == 0;
1235 }
1236 }
1237
1238 /**
1239 * A Table represents an SQL table. All data required to
1240 * generate a fully qualified name is stored, as well as dependency
1241 * data.
1242 */
1243 final class Table {
1244 final String schem;
1245 final String name;
1246 final String type;
1247 final String fqname;
1248 final ArrayList<Table> needs = new ArrayList<Table>();
1249
1250 Table(final String schem, final String name, final String type) {
1251 this.schem = schem;
1252 this.name = name;
1253 this.type = type;
1254 this.fqname = schem + "." + name;
1255 }
1256
1257 void addDependency(final Table dependsOn) throws Exception {
1258 if (this.fqname.equals(dependsOn.fqname))
1259 throw new Exception("Cyclic dependency graphs are not supported (foreign key relation references self)");
1260
1261 if (dependsOn.needs.contains(this))
1262 throw new Exception("Cyclic dependency graphs are not supported (foreign key relation a->b and b->a)");
1263
1264 if (!needs.contains(dependsOn))
1265 needs.add(dependsOn);
1266 }
1267
1268 List<Table> requires(final List<Table> existingTables) {
1269 if (existingTables == null || existingTables.isEmpty())
1270 return new ArrayList<Table>(needs);
1271
1272 final ArrayList<Table> req = new ArrayList<Table>();
1273 for (Table n : needs) {
1274 if (!existingTables.contains(n))
1275 req.add(n);
1276 }
1277
1278 return req;
1279 }
1280
1281 final String getSchem() {
1282 return schem;
1283 }
1284
1285 final String getName() {
1286 return name;
1287 }
1288
1289 final String getType() {
1290 return type;
1291 }
1292
1293 final String getFqname() {
1294 return fqname;
1295 }
1296
1297 final String getFqnameQ() {
1298 return JdbcClient.dq(schem) + "." + JdbcClient.dq(name);
1299 }
1300
1301 public final String toString() {
1302 return fqname;
1303 }
1304
1305 static final Table findTable(final String schname, final String tblname, final List<Table> list) {
1306 for (Table t : list) {
1307 if (t.schem.equals(schname) && t.name.equals(tblname))
1308 return t;
1309 }
1310 // not found
1311 return null;
1312 }
1313
1314 static final void checkForLoop(final Table table, final List<Table> parents) throws Exception {
1315 parents.add(table);
1316 for (int i = 0; i < table.needs.size(); i++) {
1317 Table child = table.needs.get(i);
1318 if (parents.contains(child))
1319 throw new Exception("Cyclic dependency graphs are not supported (cycle detected for " + child.fqname + ")");
1320 checkForLoop(child, parents);
1321 }
1322 }
1323 }
1324