Mercurial > hg > monetdb-java
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<opt>] | |
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 * <option>=<value> 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<timestamp>.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 |