view src/main/java/org/monetdb/client/JdbcClient.java @ 943:ff075ed5ce81

Spell check.
author Sjoerd Mullender <sjoerd@acm.org>
date Thu, 09 Jan 2025 10:56:14 +0100 (2 months ago)
parents d416e9b6b3d0
children 5cc071c5c170
line wrap: on
line source
/*
 * SPDX-License-Identifier: MPL-2.0
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 2024, 2025 MonetDB Foundation;
 * Copyright August 2008 - 2023 MonetDB B.V.;
 * Copyright 1997 - July 2008 CWI.
 */

package org.monetdb.client;

import org.monetdb.jdbc.MonetDriver;
import org.monetdb.jdbc.MonetConnection;
import org.monetdb.util.CmdLineOpts;
import org.monetdb.util.Exporter;
import org.monetdb.util.FileTransferHandler;
import org.monetdb.util.MDBvalidator;
import org.monetdb.util.OptionsException;
import org.monetdb.util.SQLExporter;
import org.monetdb.util.XMLExporter;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.Console;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.File;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;	// this import is required as it will trigger loading the org.monetdb.jdbc.MonetDriver class
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

/**
 * This program acts like an extended client program for MonetDB. Its
 * look and feel is very much like PostgreSQL's interactive terminal
 * program.  Although it looks like this client is designed for MonetDB,
 * it demonstrates the power of the JDBC interface since it built on top
 * of JDBC only.
 *
 * @author Fabian Groffen
 * @author Martin van Dinther
 * @version 1.8
 */

public final class JdbcClient {

	private static Connection con;
	private static DatabaseMetaData dbmd;
	private static Statement stmt;
	private static BufferedReader in;
	private static PrintWriter out;
	private static Exporter exporter;

	/**
	 * JdbcClient is a command line query tool for MonetDB, similar to mclient.
	 * It uses the JDBC API and the MonetDB JDBC driver to communicate with a
	 * MonetDB server. The MonetDB JDBC driver is included in the jdbcclient.jre8.jar
	 * for ease of use, so only 1 jar file is needed to use it.
	 *
	 * <pre>Usage java -jar jdbcclient.jre8.jar
	 *		[-h host[:port]] [-p port] [-f file] [-u user]
	 *		[-l language] [-d database] [-e] [-D [table]]
	 *		[--csvdir /path/to/csvfiles] [-X&lt;opt&gt;]
	 *		| [--help] | [--version]
	 * or using long option equivalents --host --port --file --user --language
	 * --dump --echo --database.
	 * Arguments may be written directly after the option like -p50000.
	 *
	 * If no host and port are given, localhost and 50000 are assumed.
	 * An .monetdb file may exist in the user's home directory.  This file can contain
	 * preferences to use each time JdbcClient is started.  Options given on the
	 * command line override the preferences file.  The .monetdb file syntax is
	 * &lt;option&gt;=&lt;value&gt; where option is one of the options host, port, file, mode
	 * debug, or password.  Note that the last one is perilous and therefore not
	 * available as command line option.
	 * If no input file is given using the -f flag, an interactive session is
	 * started on the terminal.
	 *
	 * OPTIONS
	 * -h --host     The hostname of the host that runs the MonetDB database.  A port
	 *               number can be supplied by use of a colon, i.e. -h somehost:12345.
	 * -p --port     The port number to connect to.
	 * -f --file     A file name to use either for reading or writing.  The file will
	 *               be used for writing when dump mode is used (-D --dump).  In read
	 *               mode, the file can also be an URL pointing to a plain text file
	 *               that is optionally gzip compressed.
	 * -u --user     The username to use when connecting to the database.
	 * -d --database Try to connect to the given database (only makes sense if
	 *               connecting to monetdbd).
	 * -l --language Use the given language, defaults to 'sql'.
	 * --help        This help screen.
	 * --version     Display driver version and exit.
	 * -e --echo     Also outputs the contents of the input file, if any.
	 * -q --quiet    Suppress printing the welcome header.
	 * -D --dump     Dumps the given table(s), or the complete database if none given.
	 * --csvdir      The directory path where csv data files will be read from or
	 *               written to when COPY ... ON CLIENT commands are executed.
	 * -Xoutput      The output mode when dumping.  Default is sql, xml may be used for
	 *               an experimental XML output.
	 * -Xhash        Use the given hash algorithm during challenge response.
	 *               Supported algorithm names: SHA512, SHA384, SHA256 and SHA1.
	 * -Xdebug       Writes a transmission log to disk for debugging purposes.  If a
	 *               file name is given, it is used, otherwise a file called
	 *               monet&lt;timestamp&gt;.log is created.  A given file never be
	 *               overwritten; instead a unique variation of the file is used.
	 * -Xbatching    Indicates that a batch should be used instead of direct
	 *               communication with the server for each statement.  If a number is
	 *               given, it is used as batch size.  i.e. 8000 would execute the
	 *               contents on the batch after each 8000 statements read.  Batching
	 *               can greatly speedup the process of restoring a database dump.</pre>
	 *
	 * @param args optional list of startup arguments
	 * @throws Exception if uncaught exception is thrown
	 */
	public final static void main(String[] args) throws Exception {
		final Properties props = new Properties();
		final CmdLineOpts copts = new CmdLineOpts();

		// arguments which take exactly one argument
		copts.addOption("h", "host", CmdLineOpts.CAR_ONE, "localhost",
				"The hostname of the host that runs the MonetDB database.  " +
				"A port number can be supplied by use of a colon, i.e. " +
				"-h somehost:12345.");
		copts.addOption("p", "port", CmdLineOpts.CAR_ONE, "50000",
				"The port number to connect to.");
		// todo make it CAR_ONE_MANY
		copts.addOption("f", "file", CmdLineOpts.CAR_ONE, null,
				"A file name to use either for reading or writing.  The " +
				"file will be used for writing when dump mode is used " +
				"(-D --dump).  In read mode, the file can also be an URL " +
				"pointing to a plain text file that is optionally gzip " +
				"compressed.");
		copts.addOption("u", "user", CmdLineOpts.CAR_ONE, System.getProperty("user.name"),
				"The username to use when connecting to the database.");
		// this one is only here for the .monetdb file parsing, it is
		// removed before the command line arguments are parsed
		copts.addOption(null, "password", CmdLineOpts.CAR_ONE, null, null);
		copts.addOption("d", "database", CmdLineOpts.CAR_ONE, "",
				"Try to connect to the given database (only makes sense " +
				"if connecting to monetdbd).");
		copts.addOption("l", "language", CmdLineOpts.CAR_ONE, "sql",
				"Use the given language, defaults to 'sql'.");
		copts.addOption(null, "csvdir", CmdLineOpts.CAR_ONE, null,
				"The directory path where csv data files are read or " +
				"written when using ON CLIENT clause of COPY command.");

		// arguments which have no argument(s)
		copts.addOption(null, "help", CmdLineOpts.CAR_ZERO, null,
				"This help screen.");
		copts.addOption(null, "version", CmdLineOpts.CAR_ZERO, null,
				"Display driver version and exit.");
		copts.addOption("e", "echo", CmdLineOpts.CAR_ZERO, null,
				"Also outputs the contents of the input file, if any.");
		copts.addOption("q", "quiet", CmdLineOpts.CAR_ZERO, null,
				"Suppress printing the welcome header.");

		// arguments which have zero to many arguments
		copts.addOption("D", "dump", CmdLineOpts.CAR_ZERO_MANY, null,
				"Dumps the given table(s), or the complete database if " +
				"none given.");

		// extended options
		copts.addOption(null, "Xoutput", CmdLineOpts.CAR_ONE, null,
				"The output mode when dumping.  Default is sql, xml may " +
				"be used for an experimental XML output.");
		copts.addOption(null, "Xhash", CmdLineOpts.CAR_ONE, null,
				"Use the given hash algorithm during challenge response. " +
				"Supported algorithm names: SHA512, SHA384, SHA256 and SHA1.");
		// arguments which can have zero or one argument(s)
		copts.addOption(null, "Xdebug", CmdLineOpts.CAR_ZERO_ONE, null,
				"Writes a transmission log to disk for debugging purposes. " +
				"If a file name is given, it is used, otherwise a file " +
				"called monet<timestamp>.log is created.  A given file " +
				"never be overwritten; instead a unique variation of the " +
				"file is used.");
		copts.addOption(null, "Xbatching", CmdLineOpts.CAR_ZERO_ONE, null,
				"Indicates that a batch should be used instead of direct " +
				"communication with the server for each statement.  If a " +
				"number is given, it is used as batch size.  i.e. 8000 " +
				"would execute the contents on the batch after each 8000 " +
				"statements read.  Batching can greatly speedup the " +
				"process of restoring a database dump.");

		copts.addIgnored("save_history");
		copts.addIgnored("format");
		copts.addIgnored("width");

		// we store user and password in separate variables in order to
		// be able to properly act on them like forgetting the password
		// from the user's file if the user supplies a username on the
		// command line arguments
		String pass = null;
		String user = null;

		// look for a file called .monetdb in the current dir or in the
		// user's homedir and read its preferences
		File pref = new File(".monetdb");
		if (!pref.exists())
			pref = new File(System.getProperty("user.home"), ".monetdb");
		if (pref.exists()) {
			try {
				copts.processFile(pref);
			} catch (OptionsException e) {
				System.err.println("Error in " + pref.getAbsolutePath() + ": " + e.getMessage());
				System.exit(1);
			}
			user = copts.getOption("user").getArgument();
			pass = copts.getOption("password").getArgument();
		}

		// process the command line arguments, remove password option
		// first, and save the user we had at this point
		copts.removeOption("password");
		try {
			copts.processArgs(args);
		} catch (OptionsException e) {
			System.err.println("Error: " + e.getMessage());
			System.exit(1);
		}
		// we can actually compare pointers (objects) here
		if (user != copts.getOption("user").getArgument())
			pass = null;

		if (copts.getOption("help").isPresent()) {
			System.out.print(
				"Usage java -jar jdbcclient.jre8.jar\n" +
				"\t\t[-h host[:port]] [-p port] [-f file] [-u user]\n" +
				"\t\t[-l language] [-d database] [-e] [-D [table]]\n" +
				"\t\t[--csvdir /path/to/csvfiles]] [-X<opt>]\n" +
				"\t\t| [--help] | [--version]\n" +
				"or using long option equivalents --host --port --file --user --language\n" +
				"--dump --echo --database.\n" +
				"Arguments may be written directly after the option like -p50000.\n" +
				"\n" +
				"If no host and port are given, localhost and 50000 are assumed.\n" +
				"An .monetdb file may exist in the user's home directory.  This file can contain\n" +
				"preferences to use each time JdbcClient is started.  Options given on the\n" +
				"command line override the preferences file.  The .monetdb file syntax is\n" +
				"<option>=<value> where option is one of the options host, port, file, mode\n" +
				"debug, or password.  Note that the last one is perilous and therefore not\n" +
				"available as command line option.\n" +
				"If no input file is given using the -f flag, an interactive session is\n" +
				"started on the terminal.\n" +
				"\n" +
				"OPTIONS\n" +
				copts.produceHelpMessage()
				);
			System.exit(0);
		}

		if (copts.getOption("version").isPresent()) {
			// We cannot use the DatabaseMetaData here, because we
			// cannot get a Connection.  So instead, we just get the
			// values we want out of the Driver directly.
			System.out.println("JDBC Driver: v" + MonetDriver.getDriverVersion());
			System.exit(0);
		}

		// whether the semi-colon at the end of a String terminates the
		// query or not (default = yes => SQL)
		final boolean scolonterm = true;
		final boolean xmlMode = "xml".equals(copts.getOption("Xoutput").getArgument());

		// we need the password from the user, fetch it with a pseudo
		// password protector
		if (pass == null) {
			final Console syscon = System.console();
			char[] tmp = null;
			if (syscon != null) {
				tmp = syscon.readPassword("password: ");
			}
			if (tmp == null) {
				System.err.println("Invalid password!");
				System.exit(1);
			}
			pass = String.valueOf(tmp);
		}

		user = copts.getOption("user").getArgument();

		// extract hostname and port
		String host = copts.getOption("host").getArgument();
		String port = copts.getOption("port").getArgument();
		int hostColon = host.indexOf(':');
		if (hostColon > 0) {
			port = host.substring(hostColon + 1);
			host = host.substring(0, hostColon);
		}
		props.setProperty("host", host);
		props.setProperty("port", port);

		// increase the fetchsize from the default 250 to 10000
		props.setProperty("fetchsize", "10000");

		CmdLineOpts.OptionContainer oc = copts.getOption("language");
		final String lang = oc.getArgument();
		if (oc.isPresent())
			props.setProperty("language", lang);

/* Xquery is no longer functional or supported
		// set some behaviour based on the language XQuery
		if (lang.equals("xquery")) {
			scolonterm = false;	// no ; to end a statement
			if (!copts.getOption("Xoutput").isPresent())
				xmlMode = true; // the user will like xml results, most probably
		}
*/
		oc = copts.getOption("Xdebug");
		if (oc.isPresent()) {
			props.setProperty("debug", "true");
			if (oc.getArgumentCount() == 1)
				props.setProperty("logfile", "logfile=" + oc.getArgument());
		}
		oc = copts.getOption("Xhash");
		if (oc.isPresent())
			props.setProperty("hash", oc.getArgument());

		// request a connection suitable for MonetDB from the driver
		// manager note that the database specifier is only used when
		// connecting to a proxy-like service, since MonetDB itself
		// can't access multiple databases.
		con = null;
		final String database = copts.getOption("database").getArgument();
		try {
			// make sure the driver class is loaded (and thus register itself with the DriverManager)
			Class.forName("org.monetdb.jdbc.MonetDriver");

			// If the database name is a full url, use that.
			// Otherwise, construct something.
			String url;
			if (database.startsWith("jdbc:")) {
				url = database;
			} else {
				url = "jdbc:monetdb:"; // special case
				props.setProperty("database", database);
			}
			props.setProperty("user", user);
			props.setProperty("password", pass);
			con = DriverManager.getConnection(url, props);
			SQLWarning warn = con.getWarnings();
			while (warn != null) {
				System.err.println("Connection warning: " + warn.getMessage());
				warn = warn.getNextWarning();
			}
			con.clearWarnings();
		} catch (SQLException e) {
			System.err.println("Database connect failed: " + e.getMessage());
			System.exit(1);
		}

		try {
			dbmd = con.getMetaData();
		} catch (SQLException e) {
			// we ignore this because it's probably because we don't use
			// SQL language
			dbmd = null;
		}

		oc = copts.getOption("csvdir");
		if (oc.isPresent()) {
			final String csvdir = oc.getArgument();
			if (csvdir != null) {
				// check if provided csvdir is an existing dir
				// else a download of data into file will terminate the JDBC connection!!
				if (java.nio.file.Files.isDirectory(java.nio.file.Paths.get(csvdir))) {
					final FileTransferHandler FThandler = new FileTransferHandler(csvdir, Charset.defaultCharset());

					// register file data uploadHandler to allow support
					// for: COPY INTO mytable FROM 'data.csv' ON CLIENT;
					((MonetConnection) con).setUploadHandler(FThandler);

					// register file data downloadHandler to allow support
					// for: COPY select_query INTO 'data.csv' ON CLIENT;
					((MonetConnection) con).setDownloadHandler(FThandler);
				} else {
					System.err.println("Warning: provided csvdir \"" + csvdir + "\" does not exist. Ignoring csvdir setting.");
				}
			}
		}

		stmt = con.createStatement();	// is used by processInteractive(), processBatch(), doDump()

		in = new BufferedReader(new InputStreamReader(System.in));
		out = new PrintWriter(new BufferedWriter(new java.io.OutputStreamWriter(System.out)));

		// see if we will have to perform a database dump (only in SQL mode)
		if ("sql".equals(lang) && copts.getOption("dump").isPresent() && dbmd != null) {
			final int argcount = copts.getOption("dump").getArgumentCount();

			// use the given file for writing
			oc = copts.getOption("file");
			if (oc.isPresent())
				out = new PrintWriter(new BufferedWriter(new java.io.FileWriter(oc.getArgument())));

			// we only want user tables and views to be dumped (DDL and optional data), unless a specific table is requested
			final String[] types = {"TABLE","VIEW","MERGE TABLE","REMOTE TABLE","REPLICA TABLE","STREAM TABLE"};
			// Future: fetch all type names using dbmd.getTableTypes() and construct String[] with all
			// table type names excluding the SYSTEM ... ones and LOCAL TEMPORARY TABLE ones.

			// request the list of tables/views available in the current schema in the database
			ResultSet tbl = dbmd.getTables(null, con.getSchema(), null, (argcount == 0) ? types : null);
			// fetch all tables and store them in a LinkedList of Table objects
			final LinkedList<Table> tables = new LinkedList<Table>();
			while (tbl.next()) {
				tables.add(new Table(
					tbl.getString(2),	// 2 = "TABLE_SCHEM"
					tbl.getString(3),	// 3 = "TABLE_NAME"
					tbl.getString(4)));	// 4 = "TABLE_TYPE"
			}
			tbl.close();
			tbl = null;

			if (xmlMode) {
				exporter = new XMLExporter(out);
				exporter.setProperty(XMLExporter.TYPE_NIL, XMLExporter.VALUE_XSI);
			} else {
				exporter = new SQLExporter(out);
				// stick with SQL INSERT INTO commands for now
				// in the future we might do COPY INTO's here using VALUE_COPY
				exporter.setProperty(SQLExporter.TYPE_OUTPUT, SQLExporter.VALUE_INSERT);
			}
			exporter.useSchemas(true);

			// start SQL output
			if (!xmlMode)
				out.println("START TRANSACTION;\n");

			// dump specific table(s) or not?
			if (argcount > 0) { // yes we do
				final String[] dumpers = copts.getOption("dump").getArguments();
				for (int i = 0; i < tables.size(); i++) {
					Table ttmp = tables.get(i);
					for (int j = 0; j < dumpers.length; j++) {
						String dumptblnm = dumpers[j].toString();
						if (ttmp.getName().equalsIgnoreCase(dumptblnm) ||
						    ttmp.getFqname().equalsIgnoreCase(dumptblnm))
						{
							// dump the table
							doDump(out, ttmp);
						}
					}
				}
			} else {
				/* this returns everything, so including SYSTEM TABLE constraints */
				tbl = dbmd.getImportedKeys(null, null, null);
				while (tbl.next()) {
					// find FK table object  6 = "FKTABLE_SCHEM", 7 = "FKTABLE_NAME"
					Table fk = Table.findTable(tbl.getString(6), tbl.getString(7), tables);

					// find PK table object  2 = "PKTABLE_SCHEM", 3 = "PKTABLE_NAME"
					Table pk = Table.findTable(tbl.getString(2), tbl.getString(3), tables);

					// this happens when a system table has referential constraints
					if (fk == null || pk == null)
						continue;

					// add PK table dependency to FK table
					fk.addDependency(pk);
				}
				tbl.close();
				tbl = null;

				// search for cycles of type a -> (x ->)+ b probably not
				// the most optimal way, but it works by just scanning
				// every table for loops in a recursive manor
				for (Table t : tables) {
					Table.checkForLoop(t, new ArrayList<Table>());
				}

				// find the graph, at this point we know there are no
				// cycles, thus a solution exists
				for (int i = 0; i < tables.size(); i++) {
					final List<Table> needs = tables.get(i).requires(tables.subList(0, i + 1));
					if (needs.size() > 0) {
						tables.removeAll(needs);
						tables.addAll(i, needs);

						// re-evaluate this position, for there is a new
						// table now
						i--;
					}
				}

				// we now have the right order to dump tables
				for (Table t : tables) {
					// dump the table
					doDump(out, t);
				}
			}

			if (!xmlMode)
				out.println("COMMIT;");
			out.flush();

			// free resources, close the statement
			stmt.close();
			// close the connection with the database
			con.close();
			// completed database dump
			System.exit(0);
		}

		if (xmlMode) {
			exporter = new XMLExporter(out);
			exporter.setProperty(XMLExporter.TYPE_NIL, XMLExporter.VALUE_XSI);
		} else {
			exporter = new SQLExporter(out);
			// we want nice table formatted output
			exporter.setProperty(SQLExporter.TYPE_OUTPUT, SQLExporter.VALUE_TABLE);
		}
		exporter.useSchemas(false);

		try {
			// use the given file for reading
			final boolean hasFile = copts.getOption("file").isPresent();
			final boolean doEcho = hasFile && copts.getOption("echo").isPresent();
			if (hasFile) {
				final String tmp = copts.getOption("file").getArgument();
				try {
					in = getReader(tmp);
				} catch (Exception e) {
					System.err.println("Error: " + e.getMessage());
					System.exit(1);
				}

				// check for batch mode
				int batchSize = 0;
				oc = copts.getOption("Xbatching");
				if (oc.isPresent()) {
					if (oc.getArgumentCount() == 1) {
						// parse the number
						try {
							batchSize = Integer.parseInt(oc.getArgument());
						} catch (NumberFormatException ex) {
							// complain to the user
							throw new IllegalArgumentException("Illegal argument for Xbatching: " + oc.getArgument() + " is not a parseable number!");
						}
					}
					processBatch(batchSize);
				} else {
					processInteractive(true, doEcho, scolonterm, user);
				}
			} else {
				if (!copts.getOption("quiet").isPresent()) {
					// print welcome message
					out.println("Welcome to the MonetDB interactive JDBC terminal!");
					if (dbmd != null) {
						out.println("JDBC Driver: " + dbmd.getDriverName() +
							" v" + dbmd.getDriverVersion());
						out.println("Database Server: " + dbmd.getDatabaseProductName() +
							" v" + dbmd.getDatabaseProductVersion());
					}
					out.println("Current Schema: " + con.getSchema());
					out.println("Type \\q to quit (you can also use: quit or exit), \\? or \\h for a list of available commands");
					out.flush();
				}
				processInteractive(false, doEcho, scolonterm, user);
			}

			// free resources, close the statement
			stmt.close();
			// close the connection with the database
			con.close();
			// close the file (if we used a file)
			in.close();
		} catch (Exception e) {
			System.err.println("A fatal exception occurred: " + e.toString());
			e.printStackTrace(System.err);
			// at least try to close the connection properly, since it will
			// close all statements associated with it
			try {
				con.close();
			} catch (SQLException ex) {
				// ok... nice try
			}
			System.exit(1);
		}
	}

	/**
	 * Tries to interpret the given String as URL or file.  Returns the
	 * assigned BufferedReader, or throws an Exception if the given
	 * string couldn't be identified as a valid URL or file.
	 *
	 * @param uri URL or filename as String
	 * @return a BufferedReader for the uri
	 * @throws Exception if uri cannot be identified as a valid URL or file
	 */
	static BufferedReader getReader(final String uri) throws Exception {
		BufferedReader ret = null;
		URI u = null;

		// Try and parse as URL
		try {
			// Note: as of Java version 20 java.net.URL(String) constructor is deprecated.
			// https://docs.oracle.com/en/java/javase/20/docs/api/java.base/java/net/URL.html#%3Cinit%3E(java.lang.String)
			u = new java.net.URI(uri);
			// the URL must start with a scheme such as: "http://" or "https://" else u.toURL() errors
			if (u != null && u.isAbsolute()) {
				final URL url = u.toURL();
				HttpURLConnection.setFollowRedirects(true);
				final HttpURLConnection con = (HttpURLConnection)url.openConnection();
				con.setRequestMethod("GET");
				final String ct = con.getContentType();
				if ("application/x-gzip".equals(ct)) {
					// open gzip stream
					ret = new BufferedReader(new InputStreamReader(
						new java.util.zip.GZIPInputStream(con.getInputStream())));
				} else {
					// text/plain otherwise just attempt to read as is
					ret = new BufferedReader(new InputStreamReader(con.getInputStream()));
				}
			}
		} catch (IOException e) {
			// failed to open/read the url
			throw new Exception("Failed to open/read http URL: " + e.getMessage());
		} catch (Exception e) {
			if (u != null)
				throw new Exception("Invalid http URL: " + uri + "\n" + e.getMessage());
		}

		if (ret == null) {
			// uri is not a valid URI/URL, so probably a file name
			try {
				ret = new BufferedReader(new java.io.FileReader(uri));
			} catch (java.io.FileNotFoundException fnfe) {
				// the message is descriptive enough,
				// adds "(No such file or directory)" itself.
				throw new Exception(fnfe.getMessage());
			}
		}

		return ret;
	}

	/**
	 * Starts an interactive processing loop, where output is adjusted to an
	 * user session.  This processing loop is not suitable for bulk processing
	 * as in executing the contents of a file, since processing on the given
	 * input is done after each row that has been entered.
	 *
	 * @param hasFile a boolean indicating whether a file is used as input
	 * @param doEcho a boolean indicating whether to echo the given input
	 * @param scolonterm whether a ';' makes this query part complete
	 * @param user a String representing the username of the current user
	 * @throws IOException if an IO exception occurs
	 * @throws SQLException if a database related error occurs
	 */
	private static void processInteractive(
		final boolean hasFile,
		final boolean doEcho,
		final boolean scolonterm,
		final String user)
		throws IOException, SQLException
	{
		// an SQL stack keeps track of ( " and '
		final SQLStack stack = new SQLStack();
		boolean lastac = false;

		if (!hasFile) {
			lastac = con.getAutoCommit();
			out.println("auto commit mode: " + (lastac ? "on" : "off"));
			out.print(getPrompt(stack, true));
			out.flush();
		}

		String curLine;
		String query = "";
		boolean doProcess;
		boolean wasComplete = true;

		// the main (interactive) process loop
		for (long i = 1; true; i++) {
			// Manually read a line, because we want to detect an EOF
			// (ctrl-D).  Doing so allows to have a terminator for query
			// which is not based on a special character, as is the
			// problem for XQuery
			curLine = in.readLine();
			if (curLine == null) {
				out.println("");
				if (!query.isEmpty()) {
					try {
						executeQuery(query, stmt, out, !hasFile);
					} catch (SQLException e) {
						out.flush();
						do {
							if (hasFile) {
								System.err.println("Error on line " + i + ": [" + e.getSQLState() + "] " + e.getMessage());
							} else {
								System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage());
							}
							// print all error messages in the chain (if any)
						} while ((e = e.getNextException()) != null);
					}
					query = "";
					wasComplete = true;
					if (!hasFile) {
						final boolean ac = con.getAutoCommit();
						if (ac != lastac) {
							out.println("auto commit mode: " + (ac ? "on" : "off"));
							lastac = ac;
						}
						out.print(getPrompt(stack, wasComplete));
					}
					out.flush();
					// try to read again
					continue;
				} else {
					// user did ctrl-D without something in the buffer,
					// so terminate
					break;
				}
			}

			if (doEcho) {
				out.println(curLine);
				out.flush();
			}

			// a query part is a line of an SQL query
			QueryPart qp = scanQuery(curLine, stack, scolonterm);
			if (!qp.isEmpty()) {
				final String command = qp.getQuery();
				doProcess = true;
				if (wasComplete) {
					doProcess = false;
					// check for commands only when the previous row was complete
					if (command.equals("\\q") || command.equals("quit") || command.equals("exit")) {
						break;
					} else if (dbmd != null && command.startsWith("\\d")) {
						processDescribeCmd(command, scolonterm);
					} else if (command.startsWith("\\v")) {
						if (command.equals("\\vsci")) {
							MDBvalidator.validateSqlCatalogIntegrity(con, true);
						} else if (command.equals("\\vsci_noheader")) {	// used only for internal automated testing
							MDBvalidator.validateSqlCatalogIntegrity(con, false);
						} else if (command.equals("\\vsni")) {
							MDBvalidator.validateSqlNetcdfTablesIntegrity(con, true);
						} else if (command.equals("\\vsni_noheader")) {	// used only for internal automated testing
							MDBvalidator.validateSqlNetcdfTablesIntegrity(con, false);
						} else if (command.equals("\\vsgi")) {
							MDBvalidator.validateSqlGeomTablesIntegrity(con, true);
						} else if (command.equals("\\vsgi_noheader")) {	// used only for internal automated testing
							MDBvalidator.validateSqlGeomTablesIntegrity(con, false);
						} else if (command.startsWith("\\vsi ")) {
							String schema_nm = command.substring(5).trim();
							if (schema_nm.endsWith(";"))
								schema_nm = schema_nm.substring(0, schema_nm.length() - 1);
							MDBvalidator.validateSchemaIntegrity(con, schema_nm, true);
						} else if (command.startsWith("\\vsi_noheader ")) {	// used only for internal automated testing
							String schema_nm = command.substring(14);
							MDBvalidator.validateSchemaIntegrity(con, schema_nm, false);
						} else if (command.equals("\\vdbi")) {
							MDBvalidator.validateDBIntegrity(con, true);
						} else if (command.equals("\\vdbi_noheader")) {	// used only for internal automated testing
							MDBvalidator.validateDBIntegrity(con, false);
						} else {
							showCommands();
						}
					} else if (command.startsWith("\\l") || command.startsWith("\\i")) {
						String object = command.substring(2).trim();
						if (scolonterm && object.endsWith(";"))
							object = object.substring(0, object.length() - 1);
						if (object.isEmpty()) {
							System.err.println("Usage: '" + command.substring(0, 2) + "<uri>' where <uri> is a file or URL");
						} else {
							// temporarily redirect input from in
							final BufferedReader console = in;
							try {
								in = getReader(object);
								if (command.startsWith("\\l")) {
									processInteractive(true, doEcho, scolonterm, user);
								} else {
									processBatch(0);
								}
							} catch (Exception e) {
								out.flush();
								System.err.println("Error: " + e.getMessage());
							} finally {
								// put back in redirection
								in = console;
							}
						}
					} else if (command.equals("\\?") || command.equals("\\h") || command.startsWith("\\")) {
						showCommands();
					} else {
						doProcess = true;
					}
				}

				if (doProcess) {
					query += command + (qp.hasOpenQuote() ? "\\n" : " ");
					if (qp.isComplete()) {
						// strip off trailing ';'
						query = query.substring(0, query.length() - 2);
						// execute query
						try {
							executeQuery(query, stmt, out, !hasFile);
						} catch (SQLException e) {
							out.flush();
							final String startmsg = (hasFile ? ("Error on line " + i + ": [") : "Error [");
							do {
								System.err.println(startmsg + e.getSQLState() + "] " + e.getMessage());
								// print all error messages in the chain (if any)
							} while ((e = e.getNextException()) != null);
						}
						query = "";
						wasComplete = true;
					} else {
						wasComplete = false;
					}
				}
			}

			if (!hasFile) {
				final boolean ac = con.getAutoCommit();
				if (ac != lastac) {
					out.println("auto commit mode: " + (ac ? "on" : "off"));
					lastac = ac;
				}
				out.print(getPrompt(stack, wasComplete));
			}
			out.flush();
		}
	}

	private static void showCommands()
	{
		out.println("Available commands:");
		out.println("\\q       quits this program (you can also use: quit or exit)");
		if (dbmd != null) {
			out.println("\\d       list available user tables and views in current schema");
			out.println("\\dt      list available user tables in current schema");
			out.println("\\dv      list available user views in current schema");
			out.println("\\df      list available user functions in current schema");
			out.println("\\dp      list available user procedures in current schema");
			out.println("\\ds      list available user sequences in current schema");
			out.println("\\dn      list available user schemas");
			out.println("\\dS      list available system tables and views in sys/tmp/information_schema schema");
			out.println("\\dSt     list available system tables in sys/tmp schema");
			out.println("\\dSv     list available system views in sys/information_schema/logging schema");
			out.println("\\dSf     list available system functions in current schema");
			out.println("\\dSp     list available system procedures in current schema");
			out.println("\\dSn     list available system schemas");
			out.println("\\d <obj> describes the given table or view");
		}
		out.println("\\l<uri>  executes the contents of the given file or URL");
		out.println("\\i<uri>  batch executes the inserts from the given file or URL");
		out.println("\\vsci    validate sql system catalog integrity");
	//	out.println("\\vsni    validate sql system netcdf tables integrity");	// do not print as it depends on availability of netcdf library on server
	//	out.println("\\vsgi    validate sql system geom tables integrity");	// do not print as it depends on availability of geom library on server
		out.println("\\vsi <schema>  validate integrity of data in the given schema");
		out.println("\\vdbi    validate integrity of data in all user schemas in the database");
		out.println("\\? or \\h this help screen");
	}

	/**
	 * Process the commands that start with \d
	 * These commands list the available user or system tables, views, functions, procedures, schemas
	 * or user sequences
	 * or describe a specific table or view by printing the CREATE DDL statement
	 * See also showCommands()
	 * These commands are almost the same as in mclient program
	 *
	 * @param dcommand the command starting with \d
	 * @param scolonterm whether a ';' makes this query part complete
	 */
	private static void processDescribeCmd(final String dcommand, final boolean scolonterm)
	{
		final String command = dcommand.substring(1);	// remove the leading \ character first
		ResultSet rs = null;
		Statement st = null;
		try {
			if (command.equals("d") || command.equals("dt") || command.equals("dv")) {
				// list available user tables and/or views in current schema (maybe tmp schema to show temporary tables)
				boolean tablesOnly = command.equals("dt");
				boolean viewsOnly = command.equals("dv");
				rs = dbmd.getTables(null, con.getSchema(), null, null);
				while (rs.next()) {
					final String tableType = rs.getString(4);	// 4 = "TABLE_TYPE"
					boolean include = (tableType != null && !tableType.startsWith("SYSTEM "));
					if (include && tablesOnly && !tableType.contains("TABLE"))
						include = false;
					if (include && viewsOnly && !tableType.contains("VIEW"))
						include = false;
					if (include) {
						out.println(tableType + "\t" +
							optAddDqs(rs.getString(2)) + "." +	// 2 = "TABLE_SCHEM"
							optAddDqs(rs.getString(3)) );		// 3 = "TABLE_NAME"
					}
				}
			} else
			if (command.equals("dS") || command.equals("dSt") || command.equals("dSv") || command.equals("dtS") || command.equals("dvS")) {
				// list available system tables and/or views in sys/tmp/information_schema/logging schema
				boolean tablesOnly = command.contains("t");
				boolean viewsOnly = command.contains("v");
				String curSchema = con.getSchema();
				// only schemas: sys, tmp, information_schema and logging contain system tables and/or system views.
				if (!("sys".equals(curSchema) || "tmp".equals(curSchema) || "information_schema".equals(curSchema) || "logging".equals(curSchema)))
					// when currently in another schema, default to the sys schema else nothing will be listed
					curSchema = "sys";
				rs = dbmd.getTables(null, curSchema, null, null);
				while (rs.next()) {
					final String tableType = rs.getString(4);	// 4 = "TABLE_TYPE"
					boolean include = (tableType != null && tableType.startsWith("SYSTEM "));
					if (include && tablesOnly && !tableType.contains("TABLE"))
						include = false;
					if (include && viewsOnly && !tableType.contains("VIEW"))
						include = false;
					if (include) {
						out.println(tableType + "\t" +
							optAddDqs(rs.getString(2)) + "." +	// 2 = "TABLE_SCHEM"
							optAddDqs(rs.getString(3)) );		// 3 = "TABLE_NAME"
					}
				}
			} else
			if (command.equals("df") || command.equals("dp") || command.equals("dSf") || command.equals("dSp") || command.equals("dfS") || command.equals("dpS")) {
				// list available user/system functions or procedures in current schema (maybe tmp schema)
				boolean proceduresOnly = command.contains("p");
				boolean systemOnly = command.contains("S");
				// Note we cannot use: dbmd.getFunctions(null, con.getSchema(), null);  or  dbmd.getProcedures(null, con.getSchema(), null);
				// as they do not return the full function type name, nor provide information whether it is a system function or system procedure.
				// Instead we query the MonetDB system catog tables directly using:
				String qy = "SELECT DISTINCT " + (systemOnly ? "'SYSTEM ' || " : "") + "\"function_type_keyword\" as func_type, " +
					"\"schemas\".\"name\" as schem_name, \"functions\".\"name\" as func_name" +
					" FROM sys.\"functions\"" +
					" JOIN sys.\"function_types\" ON \"functions\".\"type\" = \"function_types\".\"function_type_id\"" +
					" JOIN sys.\"schemas\" ON \"functions\".\"schema_id\" = \"schemas\".\"id\"" +
					" WHERE \"functions\".\"system\" = " + (systemOnly ? "true" : "false") +
					" AND \"functions\".\"type\" " + (proceduresOnly ? "= 2" : "<> 2") +
					" AND \"schemas\".\"name\" = current_schema" +
					" ORDER BY 2, 3";
				st = con.createStatement();
				rs = st.executeQuery(qy);
				while (rs.next()) {
					out.println(rs.getString(1) + "    \t" +	// 1 = func_type
						optAddDqs(rs.getString(2)) + "." +	// 2 = schem_name
						optAddDqs(rs.getString(3)) );		// 3 = func_name
				}
			} else
			if (command.equals("ds")) {
				// list available (user) sequences in current schema
				String qy = "SELECT \"schemas\".\"name\", \"sequences\".\"name\"" +
					" FROM sys.\"sequences\"" +
					" JOIN sys.\"schemas\" ON \"sequences\".\"schema_id\" = \"schemas\".\"id\"" +
					" WHERE \"schemas\".\"name\" = current_schema" +
					" ORDER BY 1, 2";
				st = con.createStatement();
				rs = st.executeQuery(qy);
				while (rs.next()) {
					out.println("SEQUENCE  " +
						optAddDqs(rs.getString(1)) + "." +	// 1 = schemas.name
						optAddDqs(rs.getString(2)) );		// 2 = sequences.name
				}
			} else
			if (command.equals("dn") || command.equals("dSn") || command.equals("dnS")) {
				// list available user/system schemas in the database
				boolean systemOnly = command.contains("S");
				String qy = "SELECT \"name\"" +
					" FROM sys.\"schemas\"" +
					" WHERE \"system\" = " + (systemOnly ? "true" : "false") +
					" ORDER BY 1";
				String schema_type = (systemOnly ? "SYSTEM SCHEMA\t" : "SCHEMA\t");
				st = con.createStatement();
				rs = st.executeQuery(qy);
				while (rs.next()) {
					out.println(schema_type + optAddDqs(rs.getString(1)) );	// 1 = schemas.name
				}
			} else
			if (command.startsWith("d ")) {
				// describes the given table or view name. It may be a fully qualified name such as: sys."keys"
				String object = command.substring(2).trim();
				if (scolonterm && object.endsWith(";"))
					object = object.substring(0, object.length() - 1);
				if (object.isEmpty()) {
					System.err.println("Missing name of object to describe");
				} else {
					// extract the name of the schema first. If none found use current schema
					String schema;
					String obj_nm = object;
					int len;
					boolean found = false;
					final int dot = object.indexOf('.');
					if (dot > 0) {
						// use specified schema
						schema = object.substring(0, dot);
						obj_nm = object.substring(dot + 1);
						// remove potential surrounding double quotes around schema name
						len = schema.length();
						if (len > 2 && schema.charAt(0) == '"' && schema.charAt(len -1) == '"')
							schema = schema.substring(1, len -1);
					} else {
						// use current schema
						schema = con.getSchema();
					}
					// remove potential surrounding double quotes around table or view name
					len = obj_nm.length();
					if (len > 2 && obj_nm.charAt(0) == '"' && obj_nm.charAt(len -1) == '"')
						obj_nm = obj_nm.substring(1, len -1);

					// System.err.println("calling dbmd.getTables(" + schema + ", " + obj_nm + ")");
					rs = dbmd.getTables(null, schema, obj_nm, null);
					while (rs.next() && !found) {
						final String schemaName = rs.getString(2);	// 2 = "TABLE_SCHEM"
						final String tableName = rs.getString(3);	// 3 = "TABLE_NAME"
						if (obj_nm.equals(tableName) && schema.equals(schemaName)) {
							String tableType = rs.getString(4);	// 4 = "TABLE_TYPE"
							if (tableType.startsWith("SYSTEM "))
								tableType = tableType.substring(7);
							// we found it, describe it
							exporter.dumpSchema(dbmd, tableType, schemaName, tableName);
							found = true;
							break;
						}
					}
					if (!found)
						System.err.println("table or view: " + schema + "." + obj_nm + " does not exist");
				}
			} else
			if (command.startsWith("d")) {
				System.err.println("unknown sub-command for \\d: " + command.substring(1));
			} else {
				showCommands();
			}
		} catch (SQLException e) {
			out.flush();
			do {
				System.err.println("Error [" + e.getSQLState() + "]: " + e.getMessage());
				// print all error messages in the chain (if any)
			} while ((e = e.getNextException()) != null);
		} finally {
			if (rs != null) {
				try {
					rs.close();
				} catch (SQLException e) { /* ignore */ }
			}
			if (st != null) {
				try {
					st.close();
				} catch (SQLException e) { /* ignore */ }
			}
		}
	}

	private static String optAddDqs(final String name)
	{
		if (name.contains(" ") || name.contains("\t") || name.contains("\n"))
			return Exporter.dq(name);
		return name;
	}

	/**
	 * Executes the given query and prints the result tabularised to the
	 * given PrintWriter stream.  The result of this method is the
	 * default output of a query: tabular data.
	 *
	 * @param query the query to execute
	 * @param stmt the Statement to execute the query on
	 * @param out the PrintWriter to write to
	 * @param showTiming flag to specify if timing information needs to be printed
	 * @throws SQLException if a database related error occurs
	 */
	private static void executeQuery(final String query,
			final Statement stmt,
			final PrintWriter out,
			final boolean showTiming)
		throws SQLException
	{
		// warnings generated during querying
		SQLWarning warn;
		long startTime = (showTiming ? System.currentTimeMillis() : 0);
		long finishTime = 0;

		// execute the query, let the driver decide what type it is
		int aff = -1;
		boolean nextRslt = stmt.execute(query, Statement.RETURN_GENERATED_KEYS);
		if (!nextRslt)
			aff = stmt.getUpdateCount();
		do {
			if (nextRslt) {
				// we have a ResultSet, print it
				final ResultSet rs = stmt.getResultSet();

				exporter.dumpResultSet(rs);
				if (showTiming) {
					finishTime = System.currentTimeMillis();
					out.println("Elapsed Time: " + (finishTime - startTime) + " ms");
					startTime = finishTime;
				}

				// if there were warnings for this result,
				// show them!
				warn = rs.getWarnings();
				if (warn != null) {
					// force stdout to be written so the
					// warning appears below it
					out.flush();
					do {
						System.err.println("ResultSet warning: " +
							warn.getMessage());
						warn = warn.getNextWarning();
					} while (warn != null);
					rs.clearWarnings();
				}
				rs.close();
			} else if (aff != -1) {
				String timingoutput = "";
				if (showTiming) {
					finishTime = System.currentTimeMillis();
					timingoutput = ". Elapsed Time: " + (finishTime - startTime) + " ms";
					startTime = finishTime;
				}

				if (aff == Statement.SUCCESS_NO_INFO) {
					out.println("Operation successful" + timingoutput);
				} else {
					// we have an update count
					// see if a key was generated
					final ResultSet rs = stmt.getGeneratedKeys();
					final boolean hasGeneratedKeyData = rs.next();
					out.println(aff + " affected row" + (aff != 1 ? "s" : "") +
						(hasGeneratedKeyData ? ", last generated key: " + rs.getString(1) : "") +
						timingoutput);
					rs.close();
				}
			}

			out.flush();
		} while ((nextRslt = stmt.getMoreResults()) ||
			 (aff = stmt.getUpdateCount()) != -1);

		// if there were warnings for this statement show them!
		warn = stmt.getWarnings();
		while (warn != null) {
			System.err.println("Statement warning: " + warn.getMessage());
			warn = warn.getNextWarning();
		}
		stmt.clearWarnings();

		// if there were warnings for this connection show them!
		warn = con.getWarnings();
		while (warn != null) {
			// suppress warning when issuing a "set schema xyz;" command
//			if ( !(warn.getMessage()).equals("Server enabled auto commit mode while local state already was auto commit.") )
				System.err.println("Connection warning: " + warn.getMessage());
			warn = warn.getNextWarning();
		}
		con.clearWarnings();
	}

	/**
	 * Starts a processing loop optimized for processing (large) chunks of
	 * continuous data, such as input from a file.  Unlike in the interactive
	 * loop above, queries are sent only to the database if a certain batch
	 * amount is reached.  No client side query checks are made, but everything
	 * is sent to the server as-is.
	 *
	 * @param batchSize the number of items to store in the batch before
	 *		sending them to the database for execution.
	 * @throws IOException if an IO exception occurs.
	 */
	private static void processBatch(final int batchSize) throws IOException {
		final StringBuilder query = new StringBuilder(2048);
		int i = 0;
		try {
			String curLine;
			// the main loop
			for (i = 1; (curLine = in.readLine()) != null; i++) {
				query.append(curLine);
				if (curLine.endsWith(";")) {
					// lousy check for end of statement, but in batch mode it
					// is not very important to catch all end of statements...
					stmt.addBatch(query.toString());
					query.setLength(0);	// clear the buffer
				} else {
					query.append('\n');
				}
				if (batchSize > 0 && i % batchSize == 0) {
					stmt.executeBatch();
					// stmt.clearBatch();	// this is no longer needed after call executeBatch(), see https://github.com/MonetDB/MonetDB/issues/6953
				}
			}
			stmt.addBatch(query.toString());
			stmt.executeBatch();
			// stmt.clearBatch();	// this is no longer needed after call executeBatch(), see https://github.com/MonetDB/MonetDB/issues/6953
		} catch (SQLException e) {
			do {
				System.err.println("Error at line " + i + ": [" + e.getSQLState() + "] " + e.getMessage());
				// print all error messages in the chain (if any)
			} while ((e = e.getNextException()) != null);
		}
	}

	/**
	 * Wrapper method that decides to dump SQL or XML.  In the latter case,
	 * this method does the XML data generation.
	 *
	 * @param out a Writer to write the data to
	 * @param table the table to dump
	 * @throws SQLException if a database related error occurs
	 */
	private static void doDump(final PrintWriter out, final Table table) throws SQLException {
		final String tableType = table.getType();

		// dump CREATE definition of this table/view
		exporter.dumpSchema(dbmd, tableType, table.getSchem(), table.getName());
		out.println();

		// only dump data from real tables, not from VIEWs / MERGE / REMOTE / REPLICA / STREAM tables
		if (tableType.contains("TABLE")
		&& !tableType.equals("MERGE TABLE")
		&& !tableType.equals("REMOTE TABLE")
		&& !tableType.equals("REPLICA TABLE")
		&& !tableType.equals("STREAM TABLE")) {
			final ResultSet rs = stmt.executeQuery("SELECT * FROM " + table.getFqnameQ());
			if (rs != null) {
				exporter.dumpResultSet(rs);
				rs.close();
				out.println();
			}
		}
	}

	/**
	 * Simple helper method that generates a prompt.
	 *
	 * @param stack the current SQLStack
	 * @param compl whether the statement is complete
	 * @return a prompt which consist of "sql" plus the top of the stack
	 */
	private static String getPrompt(final SQLStack stack, final boolean compl) {
		return (compl ? "sql" : "more") + (stack.empty() ? ">" : stack.peek()) + " ";
	}

	/**
	 * Scans the given string and tries to discover if it is a complete query
	 * or that there needs something to be added.  If a string doesn't end with
	 * a ; it is considered not to be complete.  SQL string quotation using ' and
	 * SQL identifier quotation using " is taken into account when scanning a
	 * string this way.
	 * Additionally, this method removes comments from the SQL statements,
	 * identified by -- and removes white space where appropriate.
	 *
	 * @param query the query to parse
	 * @param stack query stack to work with
	 * @param scolonterm whether a ';' makes this query part complete
	 * @return a QueryPart object containing the results of this parse
	 */
	private static QueryPart scanQuery(
			final String query,
			final SQLStack stack,
			final boolean scolonterm)
	{
		// examine string, char for char
		final boolean wasInString = (stack.peek() == '\'');
		final boolean wasInIdentifier = (stack.peek() == '"');
		boolean escaped = false;
		int len = query.length();
		for (int i = 0; i < len; i++) {
			switch(query.charAt(i)) {
				case '\\':
					escaped = !escaped;
				break;
				default:
					escaped = false;
				break;
				case '\'':
					/**
					 * We can not be in a string if we are in an identifier. So
					 * If we find a ' and are not in an identifier, and not in
					 * a string we can safely assume we will be now in a string.
					 * If we are in a string already, we should stop being in a
					 * string if we find a quote which is not prefixed by a \,
					 * for that would be an escaped quote. However, a nasty
					 * situation can occur where the string is like 'test \\'.
					 * As obvious, a test for a \ in front of a ' doesn't hold
					 * here. Because 'test \\\'' can exist as well, we need to
					 * know if a quote is prefixed by an escaping slash or not.
					 */
					if (!escaped && stack.peek() != '"') {
						if (stack.peek() != '\'') {
							// although it makes no sense to escape a quote
							// outside a string, it is escaped, thus not meant
							// as quote for us, apparently
							stack.push('\'');
						} else {
							stack.pop();
						}
					}
					// reset escaped flag
					escaped = false;
				break;
				case '"':
					if (!escaped && stack.peek() != '\'') {
						if (stack.peek() != '"') {
							stack.push('"');
						} else {
							stack.pop();
						}
					}
					// reset escaped flag
					escaped = false;
				break;
				case '-':
					if (!escaped && stack.peek() != '\'' && stack.peek() != '"' && i + 1 < len && query.charAt(i + 1) == '-') {
						len = i;
					}
					escaped = false;
				break;
				case '(':
					if (!escaped && stack.peek() != '\'' && stack.peek() != '"') {
						stack.push('(');
					}
					escaped = false;
				break;
				case ')':
					if (!escaped && stack.peek() == '(') {
						stack.pop();
					}
					escaped = false;
				break;
			}
		}

		int start = 0;
		if (!wasInString && !wasInIdentifier && len > 0) {
			// trim spaces at the start of the string
			for (; start < len && Character.isWhitespace(query.charAt(start)); start++);
		}
		int stop = len - 1;
		if (stack.peek() !=  '\'' && !wasInIdentifier && stop > start) {
			// trim spaces at the end of the string
			for (; stop >= start && Character.isWhitespace(query.charAt(stop)); stop--);
		}
		stop++;

		if (start == stop) {
			// we have an empty string
			return new QueryPart(false, null, stack.peek() ==  '\'' || stack.peek() == '"');
		} else if (stack.peek() ==  '\'' || stack.peek() == '"') {
			// we have an open quote
			return new QueryPart(false, query.substring(start, stop), true);
		} else {
			// see if the string is complete
			if (scolonterm && query.charAt(stop - 1) == ';') {
				return new QueryPart(true, query.substring(start, stop), false);
			} else {
				return new QueryPart(false, query.substring(start, stop), false);
			}
		}
	}
}

/**
 * A QueryPart is (a part of) a SQL query.  In the QueryPart object information
 * like the actual SQL query string, whether it has an open quote and the like
 * is stored.
 */
final class QueryPart {
	private final boolean complete;
	private final String query;
	private final boolean open;

	QueryPart(final boolean complete, final String query, final boolean open) {
		this.complete = complete;
		this.query = query;
		this.open = open;
	}

	boolean isEmpty() {
		return query == null;
	}

	boolean isComplete() {
		return complete;
	}

	String getQuery() {
		return query;
	}

	boolean hasOpenQuote() {
		return open;
	}
}

/**
 * An SQLStack is a simple stack that keeps track of open brackets and
 * (single and double) quotes in an SQL query.
 */
final class SQLStack {
	final StringBuilder stack = new StringBuilder();

	char peek() {
		if (empty()) {
			return '\0';
		} else {
			return stack.charAt(stack.length() - 1);
		}
	}

	char pop() {
		final char tmp = peek();
		if (tmp != '\0') {
			stack.setLength(stack.length() - 1);
		}
		return tmp;
	}

	char push(char item) {
		stack.append(item);
		return item;
	}

	boolean empty() {
		return stack.length() == 0;
	}
}

/**
 * A Table represents an SQL table.  All data required to
 * generate a fully qualified name is stored, as well as dependency
 * data.
 */
final class Table {
	final String schem;
	final String name;
	final String type;
	final String fqname;
	final ArrayList<Table> needs = new ArrayList<Table>();

	Table(final String schem, final String name, final String type) {
		this.schem = schem;
		this.name = name;
		this.type = type;
		this.fqname = schem + "." + name;
	}

	void addDependency(final Table dependsOn) throws Exception {
		if (this.fqname.equals(dependsOn.fqname))
			throw new Exception("Cyclic dependency graphs are not supported (foreign key relation references self)");

		if (dependsOn.needs.contains(this))
			throw new Exception("Cyclic dependency graphs are not supported (foreign key relation a->b and b->a)");

		if (!needs.contains(dependsOn))
			needs.add(dependsOn);
	}

	List<Table> requires(final List<Table> existingTables) {
		if (existingTables == null || existingTables.isEmpty())
			return new ArrayList<Table>(needs);

		final ArrayList<Table> req = new ArrayList<Table>();
		for (Table n : needs) {
			if (!existingTables.contains(n))
				req.add(n);
		}

		return req;
	}

	final String getSchem() {
		return schem;
	}

	final String getName() {
		return name;
	}

	final String getType() {
		return type;
	}

	final String getFqname() {
		return fqname;
	}

	final String getFqnameQ() {
		return Exporter.dq(schem) + "." + Exporter.dq(name);
	}

	public final String toString() {
		return fqname;
	}

	static final Table findTable(final String schname, final String tblname, final List<Table> list) {
		for (Table t : list) {
			if (t.schem.equals(schname) && t.name.equals(tblname))
				return t;
		}
		// not found
		return null;
	}

	static final void checkForLoop(final Table table, final List<Table> parents) throws Exception {
		parents.add(table);
		for (int i = 0; i < table.needs.size(); i++) {
			Table child = table.needs.get(i);
			if (parents.contains(child))
				throw new Exception("Cyclic dependency graphs are not supported (cycle detected for " + child.fqname + ")");
			checkForLoop(child, parents);
		}
	}
}