view src/main/java/org/monetdb/jdbc/MonetStatement.java @ 696:07d60185eeb9

Eliminate hardcoded value 250 in the constructor of MonetPreparedStatement. For this an internal package method MonetConnection.getDefaultFetchSize() is added. Also use this method in constructor of MonetStatement to let getFetchSize() return the proper fetchSize instead of 0.
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Wed, 23 Nov 2022 20:28:17 +0100 (2022-11-23)
parents 2233b172e06d
children bdeabbd46ec6
line wrap: on
line source
/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0.  If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright 1997 - July 2008 CWI, August 2008 - 2022 MonetDB B.V.
 */

package org.monetdb.jdbc;

import org.monetdb.mcl.net.MapiSocket;
import java.sql.BatchUpdateException;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.SQLWarning;
import java.util.ArrayList;
import java.util.concurrent.locks.ReentrantLock;

/**
 *<pre>
 * A {@link Statement} suitable for the MonetDB database.
 *
 * The object used for executing a static SQL statement and returning
 * the result(s) it produces.
 *
 * By default, only one {@link ResultSet} object per Statement object can be
 * open at the same time. Therefore, if the reading of one ResultSet
 * object is interleaved with the reading of another, each must have
 * been generated by different {@link Statement} objects. All execution
 * methods in the Statement interface implicitly close a Statement's current
 * ResultSet object if an open one exists.
 *
 * The current state of this Statement is that it only implements the
 * executeQuery() which returns a ResultSet where from results can be
 * read and executeUpdate() which returns the affected rows for DML.
 * Commit and rollback are implemented, as is the autoCommit mechanism
 * which relies on server side auto commit.
 * Multi-result queries are supported using the getMoreResults() method.
 *</pre>
 *
 * @author Fabian Groffen
 * @author Martin van Dinther
 * @version 0.9
 */
public class MonetStatement
	extends MonetWrapper
	implements Statement, AutoCloseable
{
	/** The parental Connection object */
	protected final MonetConnection connection;
	/** The last ResponseList object this Statement produced */
	private MonetConnection.ResponseList lastResponseList;
	/** The last Response that this object uses */
	MonetConnection.Response header;
	/** The warnings this Statement object generated */
	private SQLWarning warnings;
	/** Whether this Statement object is closed or not */
	protected boolean closed = false;
	/** Whether the application wants this Statement object to be pooled */
	protected boolean poolable = false;
	/** Whether this Statement should be closed if the last ResultSet closes */
	private boolean closeOnCompletion = false;
	/** The timeout (in sec) for the query to return, 0 means no timeout */
	private int queryTimeout;
	/** The size of the blocks of results to ask for at the server */
	private int fetchSize;
	/** The maximum number of rows to return in a ResultSet, 0 indicates unlimited */
	private long maxRows;
	/** The type of ResultSet to produce; i.e. forward only, random access */
	private int resultSetType = MonetResultSet.DEF_RESULTSETTYPE;
	/** The suggested direction of fetching data (implemented but not used) */
	private int fetchDirection = MonetResultSet.DEF_FETCHDIRECTION;
	/** The concurrency of the ResultSet to produce */
	private int resultSetConcurrency = MonetResultSet.DEF_CONCURRENCY;

	/** A List to hold all queries of a batch */
	private ArrayList<String> batch;
	private ReentrantLock batchLock;


	/**
	 * MonetStatement constructor which checks the arguments for validity.
	 * This constructor is only accessible to classes from the jdbc package.
	 *
	 * @param connection the connection that created this Statement
	 * @param resultSetType type of ResultSet to produce
	 * @param resultSetConcurrency concurrency of ResultSet to produce
	 * @param resultSetHoldability holdability of ResultSet after commit
	 * @throws SQLException if an error occurs during login
	 * @throws IllegalArgumentException is one of the arguments null or empty
	 */
	MonetStatement(
		final MonetConnection connection,
		final int resultSetType,
		final int resultSetConcurrency,
		final int resultSetHoldability)
		throws SQLException, IllegalArgumentException
	{
		if (connection == null)
			throw new IllegalArgumentException("No Connection given!");

		this.connection = connection;
		this.queryTimeout = connection.lastSetQueryTimeout;
		this.fetchSize = connection.getDefaultFetchSize();

		this.resultSetType = resultSetType;
		this.resultSetConcurrency = resultSetConcurrency;

		// check our limits, and generate warnings as appropriate
		if (this.resultSetConcurrency != ResultSet.CONCUR_READ_ONLY) {
			addWarning("No concurrency mode other then read only is supported, continuing with concurrency level READ_ONLY", "01M13");
			this.resultSetConcurrency = ResultSet.CONCUR_READ_ONLY;
		}

		// check type for supported mode
		if (this.resultSetType == ResultSet.TYPE_SCROLL_SENSITIVE) {
			addWarning("Change sensitive scrolling ResultSet objects are not supported, continuing with a change non-sensitive scrollable cursor.", "01M14");
			this.resultSetType = ResultSet.TYPE_SCROLL_INSENSITIVE;
		}

		// check type for supported holdability
		if (resultSetHoldability != ResultSet.HOLD_CURSORS_OVER_COMMIT) {
			addWarning("Close cursors at commit not supported, continuing with holdability to hold cursors open over commit.", "01M15");
		}
	}

	//== methods of interface Statement

	/**
	 * Adds the given SQL command to the current list of commmands for this
	 * Statement object.  The commands in this list can be executed as a
	 * batch by calling the method executeBatch.
	 *
	 * @param sql typically this is a static SQL INSERT or UPDATE statement
	 * @throws SQLException so the PreparedStatement can throw this exception
	 */
	@Override
	public void addBatch(final String sql) throws SQLException {
		if (sql == null || sql.isEmpty())
			throw new SQLException("Missing SQL statement", "M1M05");

		if (batch == null) {
			// create the ArrayList at first time use
			batch = new ArrayList<String>();
		}
		batch.add(sql);
	}

	/**
	 * Empties this Statement object's current list of SQL commands.
	 */
	@Override
	public void clearBatch() {
		if (batch != null)
			batch.clear();
	}

	/**
	 * Submits a batch of commands to the database for execution and if
	 * all commands execute successfully, returns an array of update
	 * counts.  The int elements of the array that is returned are
	 * ordered to correspond to the commands in the batch, which are
	 * ordered according to the order in which they were added to the
	 * batch.  The elements in the array returned by the method
	 * executeBatch may be one of the following:
	 * <ol>
	 * <li>A number greater than or equal to zero -- indicates that the
	 * command was processed successfully and is an update count giving
	 * the number of rows in the database that were affected by the
	 * command's execution
	 * <li>A value of SUCCESS_NO_INFO -- indicates that the command was
	 * processed successfully but that the number of rows affected is unknown
	 * <p>If one of the commands in a batch update fails to execute properly,
	 * this method throws a BatchUpdateException, and a JDBC driver may or
	 * may not continue to process the remaining commands in the batch.
	 * However, the driver's behavior must be consistent with a particular DBMS,
	 * either always continuing to process commands or never continuing to process
	 * commands. If the driver continues processing after a failure, the array
	 * returned by the method BatchUpdateException.getUpdateCounts will
	 * contain as many elements as there are commands in the batch, and at
	 * least one of the elements will be the following:</p>
	 * <li>A value of EXECUTE_FAILED -- indicates that the command failed to
	 * execute successfully and occurs only if a driver continues to process
	 * commands after a command fails
	 * </ol>
	 *
	 * MonetDB does continues after an error has occurred in the batch.
	 * If one of the commands attempts to return a result set, an
	 * SQLException is added to the SQLException list and thrown
	 * afterwards execution.  Failing queries result in SQLExceptions
	 * too and may cause subparts of the batch to fail as well.
	 *
	 * @return an array of update counts containing one element for each
	 *         command in the batch.  The elements of the array are ordered
	 *         according to the order in which commands were added to the batch.
	 * @throws SQLException if a database access error occurs.
	 * @throws BatchUpdateException (a subclass of SQLException) if one of the
	 *         commands sent to the database fails to execute properly.
	 */
	@Override
	public int[] executeBatch() throws SQLException {
		if (batch == null || batch.isEmpty()) {
			return new int[0];
		}

		final long[] ret = executeLargeBatch();
		// copy contents of long[] into new int[]
		final int[] counts = new int[ret.length];
		for (int i = 0; i < ret.length; i++) {
			if (ret[i] >= Integer.MAX_VALUE)
				counts[i] = Integer.MAX_VALUE;
			else
				counts[i] = (int)ret[i];
		}
		return counts;
	}

	/**
	 * Cancels this Statement object if both the DBMS and driver support aborting an SQL statement.
	 * This method can be used by one thread to cancel a statement that is being executed by another thread.
	 *
	 * @throws SQLException - if a database access error occurs or this method is called on a closed Statement
	 * @throws SQLFeatureNotSupportedException - if the JDBC driver does not support this method
	 */
	@Override
	public void cancel() throws SQLException {
		throw new SQLFeatureNotSupportedException("Query cancelling is currently not supported by the driver.", "0A000");
		// a request to implement this is already logged, see: https://www.monetdb.org/bugzilla/show_bug.cgi?id=6222
	}

	/**
	 * Clears all warnings reported for this Statement object. After a call to
	 * this method, the method getWarnings returns null until a new warning is
	 * reported for this Statement object.
	 */
	@Override
	public void clearWarnings() {
		warnings = null;
	}

	/**
	 * Releases this Statement object's database and JDBC resources immediately
	 * instead of waiting for this to happen when it is automatically closed. It
	 * is generally good practice to release resources as soon as you are
	 * finished with them to avoid tying up database resources.
	 *
	 * Calling the method close on a Statement object that is already closed has
	 * no effect.
	 *
	 * A Statement object is automatically closed when it is garbage collected.
	 * When a Statement object is closed, its current ResultSet object, if one
	 * exists, is also closed.
	 */
	@Override
	public void close() {
		clearBatch();
		clearWarnings();
		// close previous ResultSet, if not closed already
		if (lastResponseList != null) {
			lastResponseList.close();
			lastResponseList = null;
		}
		header = null;
		batchLock = null;
		closed = true;
	}

	// Chapter 13.1.2.3 of Sun's JDBC 3.0 Specification
	/**
	 * Executes the given SQL statement, which may return multiple results. In
	 * some (uncommon) situations, a single SQL statement may return multiple
	 * result sets and/or update counts. Normally you can ignore this unless
	 * you are (1) executing a stored procedure that you know may return
	 * multiple results or (2) you are dynamically executing an unknown SQL
	 * string.
	 *
	 * The execute method executes an SQL statement and indicates the form of
	 * the first result. You must then use the methods getResultSet or
	 * getUpdateCount to retrieve the result, and getMoreResults to move to any
	 * subsequent result(s).
	 *
	 * @param sql any SQL statement
	 * @return true if the first result is a ResultSet object; false if it is an
	 *         update count or there are no results
	 * @throws SQLException if a database access error occurs
	 */
	@Override
	public boolean execute(final String sql) throws SQLException {
		return internalExecute(sql);
	}

	/**
	 * Executes the given SQL statement, which may return multiple
	 * results, and signals the driver that any auto-generated keys
	 * should be made available for retrieval.  The driver will ignore
	 * this signal if the SQL statement is not an INSERT statement.
	 *
	 * In some (uncommon) situations, a single SQL statement may return
	 * multiple result sets and/or update counts. Normally you can
	 * ignore this unless you are (1) executing a stored procedure that
	 * you know may return multiple results or (2) you are dynamically
	 * executing an unknown SQL string.
	 *
	 * The execute method executes an SQL statement and indicates the
	 * form of the first result. You must then use the methods
	 * getResultSet or getUpdateCount to retrieve the result, and
	 * getMoreResults to move to any subsequent result(s).
	 *
	 * @param sql any SQL statement
	 * @param autoGeneratedKeys a constant indicating whether
	 *        auto-generated keys should be made available for retrieval
	 *        using the method getGeneratedKeys; one of the following
	 *        constants: Statement.RETURN_GENERATED_KEYS or
	 *        Statement.NO_GENERATED_KEYS
	 * @return true if the first result is a ResultSet object; false if
	 *         it is an update count or there are no results
	 * @throws SQLException - if a database access error occurs or the
	 *         second parameter supplied to this method is not
	 *         Statement.RETURN_GENERATED_KEYS or
	 *         Statement.NO_GENERATED_KEYS.
	 */
	@Override
	public boolean execute(final String sql, final int autoGeneratedKeys)
		throws SQLException
	{
		if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS &&
		    autoGeneratedKeys != Statement.NO_GENERATED_KEYS)
			throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05");

		/* MonetDB has no way to disable this, so just do the normal thing ;) */
		return internalExecute(sql);
	}

	/**
	 * Executes the given SQL statement, which may return multiple
	 * results, and signals the driver that the auto-generated keys
	 * indicated in the given array should be made available for
	 * retrieval. This array contains the indexes of the columns in the
	 * target table that contain the auto-generated keys that should be
	 * made available. The driver will ignore the array if the given SQL
	 * statement is not an INSERT statement.
	 *
	 * Under some (uncommon) situations, a single SQL statement may
	 * return multiple result sets and/or update counts. Normally you
	 * can ignore this unless you are (1) executing a stored procedure
	 * that you know may return multiple results or (2) you are
	 * dynamically executing an unknown SQL string.
	 *
	 * The execute method executes an SQL statement and indicates the
	 * form of the first result. You must then use the methods
	 * getResultSet or getUpdateCount to retrieve the result, and
	 * getMoreResults to move to any subsequent result(s).
	 *
	 * MonetDB only supports returing the generated key for one column,
	 * which will be the first column that has a serial.  Hence, this
	 * method cannot work as required and the driver will fall back to
	 * executing with request to the database to return the generated
	 * key, if any.
	 *
	 * @param sql any SQL statement
	 * @param columnIndexes an array of the indexes of the columns in
	 *        the inserted row that should be made available for
	 *        retrieval by a call to the method getGeneratedKeys
	 * @return true if the first result is a ResultSet object; false if
	 *         it is an update count or there are no results
	 * @throws SQLException if a database access error occurs or the
	 *         elements in the int array passed to this method are not
	 *         valid column indexes
	 */
	@Override
	public boolean execute(final String sql, final int[] columnIndexes)
		throws SQLException
	{
		addWarning("execute: generated keys for fixed set of columns not supported", "01M18");
		return execute(sql, Statement.RETURN_GENERATED_KEYS);
	}

	/**
	 * Executes the given SQL statement, which may return multiple
	 * results, and signals the driver that the auto-generated keys
	 * indicated in the given array should be made available for
	 * retrieval. This array contains the names of the columns in the
	 * target table that contain the auto-generated keys that should be
	 * made available. The driver will ignore the array if the given SQL
	 * statement is not an INSERT statement.
	 *
	 * In some (uncommon) situations, a single SQL statement may return
	 * multiple result sets and/or update counts. Normally you can
	 * ignore this unless you are (1) executing a stored procedure that
	 * you know may return multiple results or (2) you are dynamically
	 * executing an unknown SQL string.
	 *
	 * The execute method executes an SQL statement and indicates the
	 * form of the first result. You must then use the methods
	 * getResultSet or getUpdateCount to retrieve the result, and
	 * getMoreResults to move to any subsequent result(s).
	 *
	 * MonetDB only supports returing the generated key for one column,
	 * which will be the first column that has a serial.  Hence, this
	 * method cannot work as required and the driver will fall back to
	 * executing with request to the database to return the generated
	 * key, if any.
	 *
	 * @param sql any SQL statement
	 * @param columnNames an array of the names of the columns in the
	 *        inserted row that should be made available for retrieval
	 *        by a call to the method getGeneratedKeys
	 * @return true if the next result is a ResultSet object; false if
	 *         it is an update count or there are no more results
	 * @throws SQLException if a database access error occurs or the
	 *         elements of the String array passed to this method are
	 *         not valid column names
	 */
	@Override
	public boolean execute(final String sql, final String[] columnNames)
		throws SQLException
	{
		addWarning("execute: generated keys for fixed set of columns not supported", "01M18");
		return execute(sql, Statement.RETURN_GENERATED_KEYS);
	}

	/**
	 * Performs the steps to execute a given SQL statement.  This method
	 * exists to allow the functionality of this function to be called
	 * from within this class only.  The PreparedStatement for example
	 * overrides the execute() method to throw an SQLException, but it
	 * needs its functionality when the executeBatch method (which is
	 * inherited) is called.
	 *
	 * @param sql any SQL statement
	 * @return true if the first result is a ResultSet object; false if
	 *         it is an update count or there are no results
	 * @throws SQLException if a database access error occurs
	 */
	private boolean internalExecute(final String sql) throws SQLException {
		// close previous query, if not closed already
		if (lastResponseList != null) {
			lastResponseList.close();
			lastResponseList = null;
		}

		if (sql == null || sql.isEmpty())
			throw new SQLException("Missing SQL statement", "M1M05");

		if (queryTimeout != connection.lastSetQueryTimeout) {
			// set requested/changed queryTimeout on the server side first
			connection.setQueryTimeout(queryTimeout);
		}

		// create a container for the result
		lastResponseList = connection.new ResponseList(
			fetchSize,
			maxRows,
			resultSetType,
			resultSetConcurrency
		);
		// fill the header list by processing the query
		lastResponseList.processQuery(sql);

		return getMoreResults();
	}

	/**
	 * Executes the given SQL query, which returns a single ResultSet object.
	 *
	 * @param sql an SQL query statement to be sent to the database, typically a
	 *        static SQL SELECT statement
	 * @return a ResultSet object that contains the data produced by the given
	 *         query; never null
	 * @throws SQLException if a database access error occurs or the given SQL
	 *         statement produces anything other than a single ResultSet object
	 */
	@Override
	public ResultSet executeQuery(final String sql) throws SQLException {
		if (execute(sql) != true)
			throw new SQLException("Query did not produce a result set", "M1M19");

		return getResultSet();
	}

	/**
	 * Executes the given SQL statement, which may be an INSERT, UPDATE, or
	 * DELETE statement or an SQL statement that returns nothing, such as an
	 * SQL DDL statement.
	 *
	 * @param sql an SQL INSERT, UPDATE or DELETE statement or an SQL statement
	 *        that returns nothing
	 * @return either the row count for INSERT, UPDATE  or DELETE statements, or
	 *         0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs or the given SQL
	 *         statement produces a ResultSet object
	 */
	@Override
	public int executeUpdate(final String sql) throws SQLException {
		if (execute(sql) != false)
			throw new SQLException("Statement produced a result set", "M1M17");

		return getUpdateCount();
	}

	/**
	 * Executes the given SQL statement and signals the driver with the
	 * given flag about whether the auto-generated keys produced by this
	 * Statement object should be made available for retrieval.
	 *
	 * @param sql must be an SQL INSERT, UPDATE or DELETE statement or
	 *        an SQL statement that returns nothing
	 * @param autoGeneratedKeys - a flag indicating whether
	 *        auto-generated keys should be made available for
	 *        retrieval; one of the following constants:
	 *        Statement.RETURN_GENERATED_KEYS
	 *        Statement.NO_GENERATED_KEYS
	 * @return either the row count for INSERT, UPDATE  or DELETE
	 *         statements, or 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, the
	 *         given SQL statement returns a ResultSet object, or the
	 *         given constant is not one of those allowed
	 */
	@Override
	public int executeUpdate(final String sql, final int autoGeneratedKeys)
		throws SQLException
	{
		if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS &&
		    autoGeneratedKeys != Statement.NO_GENERATED_KEYS)
			throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05");

		/* MonetDB has no way to disable this, so just do the normal thing ;) */
		if (execute(sql) != false)
			throw new SQLException("Statement produced a result set", "M1M17");

		return getUpdateCount();
	}

	/**
	 * Executes the given SQL statement and signals the driver that the
	 * auto-generated keys indicated in the given array should be made
	 * available for retrieval. The driver will ignore the array if the
	 * SQL statement is not an INSERT statement.
	 *
	 * MonetDB only supports returing the generated key for one column,
	 * which will be the first column that has a serial.  Hence, this
	 * method cannot work as required and the driver will fall back to
	 * executing with request to the database to return the generated
	 * key, if any.
	 *
	 * @param sql an SQL INSERT, UPDATE or DELETE statement or an SQL
	 *        statement that returns nothing, such as an SQL DDL statement
	 * @param columnIndexes an array of column indexes indicating the
	 *        columns that should be returned from the inserted row
	 * @return either the row count for INSERT, UPDATE, or DELETE
	 *         statements, or 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, the SQL
	 *         statement returns a ResultSet object, or the second
	 *         argument supplied to this method is not an int array
	 *         whose elements are valid column indexes
	 */
	@Override
	public int executeUpdate(final String sql, final int[] columnIndexes)
		throws SQLException
	{
		addWarning("executeUpdate: generated keys for fixed set of columns not supported", "01M18");
		return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
	}

	/**
	 * Executes the given SQL statement and signals the driver that the
	 * auto-generated keys indicated in the given array should be made
	 * available for retrieval. The driver will ignore the array if the
	 * SQL statement is not an INSERT statement.
	 *
	 * MonetDB only supports returing the generated key for one column,
	 * which will be the first column that has a serial.  Hence, this
	 * method cannot work as required and the driver will fall back to
	 * executing with request to the database to return the generated
	 * key, if any.
	 *
	 * @param sql an SQL INSERT, UPDATE or DELETE statement or an SQL
	 *        statement that returns nothing, such as an SQL DDL statement
	 * @param columnNames an array of the names of the columns that
	 *        should be returned from the inserted row
	 * @return either the row count for INSERT, UPDATE, or DELETE
	 *         statements, or 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, the SQL
	 *         statement returns a ResultSet object, or the second
	 *         argument supplied to this method is not a String array
	 *         whose elements are valid column names
	 */
	@Override
	public int executeUpdate(final String sql, final String[] columnNames)
		throws SQLException
	{
		addWarning("executeUpdate: generated keys for fixed set of columns not supported", "01M18");
		return executeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
	}

	/**
	 * Retrieves the Connection object that produced this Statement object.
	 *
	 * @return the connection that produced this statement
	 */
	@Override
	public Connection getConnection() {
		return connection;
	}

	/**
	 * Retrieves the direction for fetching rows from database tables that is
	 * the default for result sets generated from this Statement object. If
	 * this Statement object has not set a fetch direction by calling the
	 * method setFetchDirection, the return value is ResultSet.FETCH_FORWARD.
	 *
	 * @return the default fetch direction for result sets generated from this
	 *         Statement object
	 */
	@Override
	public int getFetchDirection() {
		return fetchDirection;
	}

	/**
	 * Retrieves the number of result set rows that is the default fetch size
	 * for ResultSet objects generated from this Statement object.
	 * If this Statement object has not set a fetch size by calling the
	 * method setFetchSize, the return value is implementation-specific.
	 *
	 * @return the default fetch size for result sets generated from this
	 *         Statement object
	 */
	@Override
	public int getFetchSize() {
		return fetchSize;
	}

	/**
	 * Retrieves any auto-generated keys created as a result of
	 * executing this Statement object.  If this Statement object did not
	 * generate any keys, an empty ResultSet object is returned.
	 *
	 * @return a ResultSet object containing the auto-generated key(s)
	 *         generated by the execution of this Statement object
	 * @throws SQLException - if a database access error occurs
	 */
	@Override
	public ResultSet getGeneratedKeys() throws SQLException {
		final String[] columns, types;
		final String[][] results;

		columns = new String[1];
		types = new String[1];

		columns[0] = "GENERATED_KEY";
		/* the generated key should be an integer, because (wait for it) other
		 * frameworks such as spring expect this. */
		types[0] = "BIGINT";

		if (header != null && header instanceof MonetConnection.UpdateResponse) {
			final String lastid = ((MonetConnection.UpdateResponse)header).lastid;
			if (lastid.equals("-1")) {
				results = new String[0][1];
			} else {
				results = new String[1][1];
				results[0][0] = lastid;
			}
		} else {
			results = new String[0][1];
		}

		try {
			return new MonetVirtualResultSet(this, columns, types, results);
		} catch (IllegalArgumentException e) {
			throw new SQLException("Internal driver error: " + e.getMessage(), "M0M03");
		}
	}

	/**
	 * Retrieves the maximum number of bytes that can be returned for
	 * character and binary column values in a ResultSet object produced
	 * by this Statement object. This limit applies only to BINARY,
	 * VARBINARY, LONGVARBINARY, CHAR, VARCHAR, and LONGVARCHAR
	 * columns. If the limit is exceeded, the excess data is silently
	 * discarded.
	 *
	 * @return the current column size limit for columns storing
	 *         character and binary values; zero means there is no limit
	 * @see #setMaxFieldSize(int max)
	 */
	@Override
	public int getMaxFieldSize() {
		return 2*1024*1024*1024 - 2;	// MonetDB supports null terminated strings of max 2GB, see function: int UTF8_strlen()
	}

	/**
	 * Retrieves the maximum number of rows that a ResultSet object produced by
	 * this Statement object can contain. If this limit is exceeded, the excess
	 * rows are silently dropped.
	 *
	 * @return the current maximum number of rows for a ResultSet object
	 *         produced by this Statement object; zero means there is no limit
	 * @see #setMaxRows(int max)
	 */
	@Override
	public int getMaxRows() {
		if (maxRows >= Integer.MAX_VALUE)
			return Integer.MAX_VALUE;
		return (int)maxRows;
	}

	/**
	 * Moves to this Statement object's next result, returns true if it is a
	 * ResultSet object, and implicitly closes any current ResultSet object(s)
	 * obtained with the method getResultSet.
	 *
	 * There are no more results when the following is true:
	 * (!getMoreResults() &amp;&amp; (getUpdateCount() == -1)
	 *
	 * @return true if the next result is a ResultSet object; false if it is
	 *              an update count or there are no more results
	 * @throws SQLException if a database access error occurs
	 * @see #getMoreResults(int current)
	 */
	@Override
	public boolean getMoreResults() throws SQLException {
		return getMoreResults(CLOSE_ALL_RESULTS);
	}

	/**
	 * Moves to this Statement object's next result, deals with any current
	 * ResultSet object(s) according to the instructions specified by the given
	 * flag, and returns true if the next result is a ResultSet object.
	 *
	 * There are no more results when the following is true:
	 * (!getMoreResults() &amp;&amp; (getUpdateCount() == -1)
	 *
	 * @param current one of the following Statement constants indicating what
	 *                should happen to current ResultSet objects obtained using
	 *                the method getResultSet: CLOSE_CURRENT_RESULT,
	 *                KEEP_CURRENT_RESULT, or CLOSE_ALL_RESULTS
	 * @return true if the next result is a ResultSet object; false if it is
	 *              an update count or there are no more results
	 * @throws SQLException if a database access error occurs
	 */
	@Override
	public boolean getMoreResults(final int current) throws SQLException {
		// protect against people calling this on an unitialised state
		if (lastResponseList == null) {
			header = null;
			return false;
		}

		if (current == CLOSE_CURRENT_RESULT) {
			lastResponseList.closeCurrentResponse();
		} else if (current == CLOSE_ALL_RESULTS) {
			lastResponseList.closeCurOldResponses();
		}
		// we default to keep current result, which requires no action
		header = lastResponseList.getNextResponse();

		return (header != null && header instanceof MonetConnection.ResultSetResponse);
	}

	/**
	 * Retrieves the number of seconds the driver will wait for a
	 * Statement object to execute.  If the limit is exceeded, a
	 * SQLException is thrown.
	 *
	 * @return the current query timeout limit in seconds; zero means
	 *         there is no limit
	 * @see #setQueryTimeout(int)
	 */
	@Override
	public int getQueryTimeout() {
		return queryTimeout;
	}

	/**
	 * Retrieves the current result as a ResultSet object.  This method
	 * should be called only once per result.
	 *
	 * @return the current result as a ResultSet object or null if the result
	 *         is an update count or there are no more results
	 * @throws SQLException if a database access error occurs
	 */
	@Override
	public ResultSet getResultSet() throws SQLException {
		if (header != null && header instanceof MonetConnection.ResultSetResponse)
			return new MonetResultSet(this, (MonetConnection.ResultSetResponse)header);
		return null;
	}

	/**
	 * Retrieves the result set concurrency for ResultSet objects generated
	 * by this Statement object.
	 *
	 * @return either ResultSet.CONCUR_READ_ONLY or ResultSet.CONCUR_UPDATABLE
	 */
	@Override
	public int getResultSetConcurrency() {
		return resultSetConcurrency;
	}

	/**
	 * Retrieves the result set holdability for ResultSet objects
	 * generated by this Statement object.
	 *
	 * @return either ResultSet.HOLD_CURSORS_OVER_COMMIT or
	 *         ResultSet.CLOSE_CURSORS_AT_COMMIT
	 */
	@Override
	public int getResultSetHoldability() {
		return ResultSet.HOLD_CURSORS_OVER_COMMIT;
	}

	/**
	 * Retrieves the result set type for ResultSet objects generated by this
	 * Statement object.
	 *
	 * @return one of ResultSet.TYPE_FORWARD_ONLY,
	 *         ResultSet.TYPE_SCROLL_INSENSITIVE, or
	 *         ResultSet.TYPE_SCROLL_SENSITIVE
	 */
	@Override
	public int getResultSetType() {
		return resultSetType;
	}

	/**
	 * Retrieves the current result as an update count; if the result is a
	 * ResultSet object or there are no more results, -1 is returned.  This
	 * method should be called only once per result.
	 *
	 * @return the current result as an update count; -1 if the current result
	 *         is a ResultSet object or there are no more results
	 * @throws SQLException if a database access error occurs
	 */
	@Override
	public int getUpdateCount() throws SQLException {
		long ret = getLargeUpdateCount();
		if (ret >= Integer.MAX_VALUE)
			return Integer.MAX_VALUE;
		return (int)ret;
	}

	/**
	 * Retrieves the first warning reported by calls on this Statement object.
	 * If there is more than one warning, subsequent warnings will be chained to
	 * the first one and can be retrieved by calling the method
	 * SQLWarning.getNextWarning on the warning that was retrieved previously.
	 *
	 * This method may not be called on a closed statement; doing so will cause
	 * an SQLException to be thrown.
	 *
	 * Note: Subsequent warnings will be chained to this SQLWarning.
	 *
	 * @return the first SQLWarning object or null if there are none
	 * @throws SQLException if a database access error occurs or this method is
	 *         called on a closed connection
	 */
	@Override
	public SQLWarning getWarnings() throws SQLException {
		if (closed)
			throw new SQLException("Cannot call on closed Statement", "M1M20");

		// if there are no warnings, this will be null, which fits with the
		// specification.
		return warnings;
	}

	/**
	 * Sets the SQL cursor name to the given String, which will be used
	 * by subsequent Statement object execute methods. This name can
	 * then be used in SQL positioned update or delete statements to
	 * identify the current row in the ResultSet object generated by
	 * this statement. If the database does not support positioned
	 * update/delete, this method is a noop. To insure that a cursor has
	 * the proper isolation level to support updates, the cursor's
	 * SELECT statement should have the form SELECT FOR UPDATE. If FOR
	 * UPDATE is not present, positioned updates may fail.
	 *
	 * <b>Note:</b> By definition, the execution of positioned updates
	 * and deletes must be done by a different Statement object than the
	 * one that generated the ResultSet object being used for
	 * positioning.  Also, cursor names must be unique within a
	 * connection.
	 *
	 * Since MonetDB does not support positioned update/delete, this
	 * method is a noop.
	 *
	 * @param name the new cursor name, which must be unique within a
	 *        connection
	 * @throws SQLException if a database access error occurs
	 */
	@Override
	public void setCursorName(final String name) throws SQLException {
		addWarning("setCursorName: positioned updates/deletes not supported", "01M21");
	}

	/**
	 * Sets escape processing on or off. If escape scanning is on (the
	 * default), the driver will do escape substitution before sending
	 * the SQL statement to the database. Note: Since prepared
	 * statements have usually been parsed prior to making this call,
	 * disabling escape processing for PreparedStatements objects will
	 * have no effect.
	 *
	 * The MonetDB JDBC driver implements no escape processing at all in
	 * its current implementation because it is too expensive, and in
	 * general should not be necessary given SQL standards compliance.
	 * In this sense, this driver will ignore any call to this function.
	 *
	 * @param enable true to enable escape processing; false to disable it
	 * @throws SQLException if a database access error occurs
	 */
	@Override
	public void setEscapeProcessing(final boolean enable) throws SQLException {
		if (enable)
			addWarning("setEscapeProcessing: JDBC escape syntax is not supported by this driver", "01M22");
	}

	/**
	 * Gives the driver a hint as to the direction in which rows will be
	 * processed in ResultSet objects created using this Statement object.
	 * The default value is ResultSet.FETCH_FORWARD.
	 *
	 * Note that this method sets the default fetch direction for result sets
	 * generated by this Statement object. Each result set has its own methods
	 * for getting and setting its own fetch direction.
	 *
	 * @param direction the initial direction for processing rows
	 * @throws SQLException if a database access error occurs or the given
	 *         direction is not one of ResultSet.FETCH_FORWARD,
	 *         ResultSet.FETCH_REVERSE, or ResultSet.FETCH_UNKNOWN
	 */
	@Override
	public void setFetchDirection(final int direction) throws SQLException {
		if (direction == ResultSet.FETCH_FORWARD ||
		    direction == ResultSet.FETCH_REVERSE ||
		    direction == ResultSet.FETCH_UNKNOWN)
		{
			fetchDirection = direction;
		} else {
			throw new SQLException("Illegal direction: " + direction, "M1M05");
		}
	}

	/**
	 * Gives the JDBC driver a hint as to the number of rows that should be
	 * fetched from the database when more rows are needed. The number of rows
	 * specified affects only ResultSet created using this Statement. If the
	 * value specified is zero, then the hint is ignored.
	 *
	 * @param rows the number of rows to fetch
	 * @throws SQLException if the condition 0 &lt;= rows &lt;= this.getMaxRows()
	 *         is not satisfied.
	 */
	@Override
	public void setFetchSize(final int rows) throws SQLException {
		if (rows >= 0 && !(maxRows != 0 && rows > getMaxRows())) {
			fetchSize = rows;
		} else {
			throw new SQLException("Illegal fetch size value: " + rows, "M1M05");
		}
	}

	/**
	 * Sets the limit for the maximum number of bytes in a ResultSet
	 * column storing character or binary values to the given number of
	 * bytes. This limit applies only to BINARY, VARBINARY,
	 * LONGVARBINARY, CHAR, VARCHAR, and LONGVARCHAR fields. If the
	 * limit is exceeded, the excess data is silently discarded. For
	 * maximum portability, use values greater than 256.
	 *
	 * MonetDB does not support any fieldsize limiting, and hence the
	 * driver does not emulate it either, since it doesn't really lead
	 * to memory reduction.
	 *
	 * @param max the new column size limit in bytes; zero means there
	 *        is no limit
	 * @throws SQLException if a database access error occurs or the
	 *         condition max &gt;= 0 is not satisfied
	 * @see #getMaxFieldSize()
	 */
	@Override
	public void setMaxFieldSize(final int max) throws SQLException {
		if (max < 0)
			throw new SQLException("Illegal max value: " + max, "M1M05");
		if (max > 0)
			addWarning("setMaxFieldSize: field size limitation not supported", "01M23");
	}

	/**
	 * Sets the limit for the maximum number of rows that any ResultSet object
	 * can contain to the given number. If the limit is exceeded, the excess
	 * rows are silently dropped.
	 *
	 * @param max the new max rows limit; zero means there is no limit
	 * @throws SQLException if the condition max &gt;= 0 is not satisfied
	 * @see #getMaxRows()
	 */
	@Override
	public void setMaxRows(final int max) throws SQLException {
		if (max < 0)
			throw new SQLException("Illegal max value: " + max, "M1M05");
		maxRows = max;
	}

	/**
	 * Sets the number of seconds the driver will wait for a Statement
	 * object to execute to the given number of seconds. If the limit is
	 * exceeded, an SQLException is thrown.
	 *
	 * @param seconds the new query timeout limit in seconds; zero means
	 *        there is no limit
	 * @throws SQLException if a database access error occurs or the
	 *         condition seconds &gt;= 0 is not satisfied
	 */
	@Override
	public void setQueryTimeout(final int seconds) throws SQLException {
		if (seconds < 0)
			throw new SQLException("Illegal timeout value: " + seconds, "M1M05");
		queryTimeout = seconds;
	}

	//== 1.6 methods (JDBC 4.0)

	/**
	 * Retrieves whether this Statement object has been closed. A
	 * Statement is closed if the method close has been called on it, or
	 * if it is automatically closed.
	 *
	 * @return true if this Statement object is closed; false if it is
	 *         still open
	 */
	@Override
	public boolean isClosed() {
		return closed;
	}

	/**
	 * Requests that a Statement be pooled or not pooled. The value
	 * specified is a hint to the statement pool implementation
	 * indicating whether the applicaiton wants the statement to be
	 * pooled. It is up to the statement pool manager as to whether the
	 * hint is used.
	 *
	 * The poolable value of a statement is applicable to both internal
	 * statement caches implemented by the driver and external statement
	 * caches implemented by application servers and other applications.
	 *
	 * By default, a Statement is not poolable when created, and a
	 * PreparedStatement and CallableStatement are poolable when
	 * created.
	 *
	 * @param poolable requests that the statement be pooled if true
	 *        and that the statement not be pooled if false
	 */
	@Override
	public void setPoolable(final boolean poolable) {
		this.poolable = poolable;
	}

	/**
	 * Returns a value indicating whether the Statement is poolable or
	 * not.
	 *
	 * @return true if the Statement is poolable; false otherwise
	 */
	@Override
	public boolean isPoolable() {
		return poolable;
	}

	//== 1.7 methods (JDBC 4.1)

	/**
	 * Specifies that this Statement will be closed when all its
	 * dependent result sets are closed.  If execution of the Statement
	 * does not produce any result sets, this method has no effect.
	 *
	 * @throws SQLException if this method is called on a closed Statement
	 */
	@Override
	public void closeOnCompletion() throws SQLException {
		if (closed)
			throw new SQLException("Cannot call on closed Statement", "M1M20");
		closeOnCompletion = true;
	}

	/**
	 * Returns a value indicating whether this Statement will be closed
	 * when all its dependent result sets are closed.
	 *
	 * @return true if the Statement will be closed when all of its
	 *         dependent result sets are closed; false otherwise
	 * @throws SQLException if this method is called on a closed Statement
	 */
	@Override
	public boolean isCloseOnCompletion() throws SQLException {
		if (closed)
			throw new SQLException("Cannot call on closed Statement", "M1M20");
		return closeOnCompletion;
	}

	//== Java 1.8 methods (JDBC 4.2)

	/**
	 * Retrieves the current result as an update count;
	 * if the result is a ResultSet object or there are no more results, -1 is returned.
	 * This method should be called only once per result.
	 *
	 * This method should be used when the returned row count may exceed Integer.MAX_VALUE.
	 * The default implementation will throw UnsupportedOperationException
	 *
	 * @return the current result as an update count; -1 if the current
	 *	result is a ResultSet object or there are no more results
	 * @throws SQLException if a database access error occurs or this
	 *	method is called on a closed Statement
	 */
	@Override
	public long getLargeUpdateCount() throws SQLException {
		if (header != null) {
			if (header instanceof MonetConnection.UpdateResponse) {
				return ((MonetConnection.UpdateResponse)header).count;
			} else if (header instanceof MonetConnection.SchemaResponse) {
				return ((MonetConnection.SchemaResponse)header).state;
			}
		}
		return -1;
	}

	/**
	 * Sets the limit for the maximum number of rows that any ResultSet object
	 * generated by this Statement object can contain to the given number.
	 * If the limit is exceeded, the excess rows are silently dropped.
	 *
	 * This method should be used when the row limit may exceed Integer.MAX_VALUE.
	 * The default implementation will throw UnsupportedOperationException
	 *
	 * @param max the new max rows limit; zero means there is no limit
	 * @throws SQLException if a database access error occurs,
	 *	this method is called on a closed Statement or
	 *	the condition max &gt;= 0 is not satisfied
	 * @see #getLargeMaxRows()
	 */
	@Override
	public void setLargeMaxRows(final long max) throws SQLException {
		if (max < 0)
			throw new SQLException("Illegal max value: " + max, "M1M05");
		maxRows = max;
	}

	/**
	 * Retrieves the maximum number of rows that a ResultSet object produced by
	 * this Statement object can contain. If this limit is exceeded, the excess
	 * rows are silently dropped.
	 *
	 * This method should be used when the returned row limit may exceed Integer.MAX_VALUE.
	 * The default implementation will return 0
	 *
	 * @return the current maximum number of rows for a ResultSet object
	 *	produced by this Statement object; zero means there is no limit
	 * @see #setLargeMaxRows(long max)
	 */
	@Override
	public long getLargeMaxRows() {
		return maxRows;
	}

	/**
	 * Submits a batch of commands to the database for execution and if
	 * all commands execute successfully, returns an array of update counts.
	 * The long elements of the array that is returned are ordered to
	 * correspond to the commands in the batch, which are ordered according
	 * to the order in which they were added to the batch.
	 * The elements in the array returned by the method executeLargeBatch
	 * may be one of the following:
	 * <ol>
	 * <li>A number greater than or equal to zero -- indicates that the command
	 * was processed successfully and is an update count giving the number of
	 * rows in the database that were affected by the command's execution
	 * <li>A value of SUCCESS_NO_INFO -- indicates that the command was
	 * processed successfully but that the number of rows affected is unknown
	 * <p>If one of the commands in a batch update fails to execute properly,
	 * this method throws a BatchUpdateException, and a JDBC driver may or
	 * may not continue to process the remaining commands in the batch.
	 * However, the driver's behavior must be consistent with a particular DBMS,
	 * either always continuing to process commands or never continuing to process
	 * commands. If the driver continues processing after a failure, the array
	 * returned by the method BatchUpdateException.getLargeUpdateCounts will
	 * contain as many elements as there are commands in the batch, and at
	 * least one of the elements will be the following:</p>
	 * <li>A value of EXECUTE_FAILED -- indicates that the command failed to
	 * execute successfully and occurs only if a driver continues to process
	 * commands after a command fails
	 * </ol>
	 *
	 * This method should be used when the returned row count may exceed Integer.MAX_VALUE.
	 * The default implementation will throw UnsupportedOperationException
	 *
	 * MonetDB does continues after an error has occurred in the batch.
	 * If one of the commands attempts to return a result set, an
	 * SQLException is added to the SQLException list and thrown
	 * afterwards execution.  Failing queries result in SQLExceptions
	 * too and may cause subparts of the batch to fail as well.
	 *
	 * @return an array of update counts containing one element for each
	 *	command in the batch. The elements of the array are ordered
	 *	according to the order in which commands were added to the batch.
	 * @throws SQLException if a database access error occurs, this method is called
	 *	on a closed Statement or the driver does not support batch statements.
	 * @throws BatchUpdateException (a subclass of SQLException) if one of the
	 *	commands sent to the database fails to execute properly or
	 *	attempts to return a result set.
	 */
	@Override
	public long[] executeLargeBatch() throws SQLException {
		if (batch == null || batch.isEmpty()) {
			return new long[0];
		}

		// this method is synchronized/locked to make sure no one gets in between the
		// operations we execute below
		if (batchLock == null) {
			// create a ReentrantLock at first time use
			batchLock = new ReentrantLock();
		}
		batchLock.lock();
		try {
			final long[] counts = new long[batch.size()];
			final String sep = connection.queryTempl[2];
			final int sepLen = sep.length();
			final BatchUpdateException e = new BatchUpdateException(
					"Error(s) occurred while executing the batch, " +
					"see chained SQLExceptions for details", "22000", 22000, counts, null);
			final StringBuilder tmpBatch = new StringBuilder(MapiSocket.BLOCK);
			int offset = 0;
			boolean first = true;
			boolean error = false;

			for (int i = 0; i < batch.size(); i++) {
				String tmp = batch.get(i);
				if (sepLen + tmp.length() > MapiSocket.BLOCK) {
					// The thing is too big.  Way too big.  Since it won't
					// be optimal anyway, just add it to whatever we have
					// and continue.
					if (!first)
						tmpBatch.append(sep);
					tmpBatch.append(tmp);
					// send and receive
					error |= internalBatch(tmpBatch, counts, offset, i + 1, e);
					offset = i;
					tmpBatch.setLength(0);	// clear the buffer
					first = true;
					continue;
				}
				if (tmpBatch.length() + sepLen + tmp.length() >= MapiSocket.BLOCK) {
					// send and receive
					error |= internalBatch(tmpBatch, counts, offset, i + 1, e);
					offset = i;
					tmpBatch.setLength(0);	// clear the buffer
					first = true;
				}
				if (first)
					first = false;
				else
					tmpBatch.append(sep);
				tmpBatch.append(tmp);
			}
			// send and receive
			error |= internalBatch(tmpBatch, counts, offset, counts.length, e);

			// throw BatchUpdateException if it contains something
			if (error)
				throw e;

			// otherwise just return the counts
			return counts;
		} finally {
			batch.clear();
			batchLock.unlock();
		}
	}

	private boolean internalBatch(
			final StringBuilder batch,
			final long[] counts,
			int offset,
			final int max,
			final BatchUpdateException e)
		throws BatchUpdateException
	{
		if (batch.length() == 0)
			return false;
		try {
			long count = -1;
			boolean hasResultSet = internalExecute(batch.toString());

			if (!hasResultSet)
				count = getLargeUpdateCount();

			do {
				if (offset >= max)
					throw new SQLException("Overflow: don't use multi statements when batching (" + max + ")", "M1M16");
				if (hasResultSet) {
					e.setNextException(
						new SQLException("Batch query produced a ResultSet! " +
							"Ignoring and setting update count to value " + EXECUTE_FAILED, "M1M17"));
					counts[offset] = EXECUTE_FAILED;
				} else if (count >= 0) {
					counts[offset] = count;
				}
				offset++;
			} while ((hasResultSet = getMoreResults()) || (count = getLargeUpdateCount()) != -1);
		} catch (SQLException ex) {
			e.setNextException(ex);
			for (; offset < max; offset++) {
				counts[offset] = EXECUTE_FAILED;
			}
			return true;
		}
		return false;
	}


	/**
	 * Executes the given SQL statement, which may be an INSERT, UPDATE, or
	 * DELETE statement or an SQL statement that returns nothing, such as an
	 * SQL DDL statement.
	 *
	 * This method should be used when the returned row count may exceed Integer.MAX_VALUE.
	 * Note: This method cannot be called on a PreparedStatement or CallableStatement.
	 * The default implementation will throw SQLFeatureNotSupportedException
	 *
	 * @param sql an SQL Data Manipulation Language (DML) statement, such as
	 *	INSERT, UPDATE or DELETE; or an SQL statement that returns nothing,
	 *	such as a DDL statement.
	 * @return either (1) the row count for SQL Data Manipulation Language (DML) statements
	 *	or (2) 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, this method is
	 *	called on a closed Statement, the given SQL statement produces a
	 *	ResultSet object, the method is called on a PreparedStatement or CallableStatement
	 */
	@Override
	public long executeLargeUpdate(final String sql) throws SQLException {
		if (execute(sql) != false)
			throw new SQLException("Statement produced a result set", "M1M17");

		return getLargeUpdateCount();
	}

	/**
	 * Executes the given SQL statement and signals the driver with the
	 * given flag about whether the auto-generated keys produced by this
	 * Statement object should be made available for retrieval.
	 * The driver will ignore the flag if the SQL statement is not an INSERT
	 * statement, or an SQL statement able to return auto-generated keys
	 * (the list of such statements is vendor-specific).
	 *
	 * This method should be used when the returned row count may exceed Integer.MAX_VALUE.
	 * Note: This method cannot be called on a PreparedStatement or CallableStatement.
	 * The default implementation will throw SQLFeatureNotSupportedException
	 *
	 * @param sql an SQL Data Manipulation Language (DML) statement, such as
	 *	INSERT, UPDATE or DELETE; or an SQL statement that returns nothing,
	 *	such as a DDL statement.
	 * @param autoGeneratedKeys - a flag indicating whether
	 *        auto-generated keys should be made available for
	 *        retrieval; one of the following constants:
	 *        Statement.RETURN_GENERATED_KEYS
	 *        Statement.NO_GENERATED_KEYS
	 * @return either (1) the row count for SQL Data Manipulation Language (DML) statements
	 *	or (2) 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, this method is
	 *	called on a closed Statement, the given SQL statement produces a
	 *	ResultSet object, the given constant is not one of those allowed,
	 *	the method is called on a PreparedStatement or CallableStatement
	 */
	@Override
	public long executeLargeUpdate(final String sql, final int autoGeneratedKeys)
		throws SQLException
	{
		if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS &&
		    autoGeneratedKeys != Statement.NO_GENERATED_KEYS)
			throw new SQLException("Invalid argument, expected RETURN_GENERATED_KEYS or NO_GENERATED_KEYS", "M1M05");

		/* MonetDB has no way to disable this, so just do the normal thing ;) */
		if (execute(sql) != false)
			throw new SQLException("Statement produced a result set", "M1M17");

		return getLargeUpdateCount();
	}

	/**
	 * Executes the given SQL statement and signals the driver that the
	 * auto-generated keys indicated in the given array should be made
	 * available for retrieval. The driver will ignore the array if the
	 * SQL statement is not an INSERT statement.
	 *
	 * This method should be used when the returned row count may exceed Integer.MAX_VALUE.
	 * Note: This method cannot be called on a PreparedStatement or CallableStatement.
	 * The default implementation will throw SQLFeatureNotSupportedException
	 *
	 * MonetDB only supports returing the generated key for one column,
	 * which will be the first column that has a serial.  Hence, this
	 * method cannot work as required and the driver will fall back to
	 * executing with request to the database to return the generated
	 * key, if any.
	 *
	 * @param sql an SQL Data Manipulation Language (DML) statement, such as
	 *	INSERT, UPDATE or DELETE; or an SQL statement that returns nothing,
	 *	such as a DDL statement.
	 * @param columnIndexes an array of column indexes indicating the
	 *        columns that should be returned from the inserted row
	 * @return either (1) the row count for SQL Data Manipulation Language (DML) statements
	 *	or (2) 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, this method is
	 *	called on a closed Statement, the given SQL statement produces a
	 *	ResultSet object, the second argument supplied to this method is
	 *	not an int array whose elements are valid column indexes,
	 *	the method is called on a PreparedStatement or CallableStatement
	 */
	@Override
	public long executeLargeUpdate(final String sql, final int[] columnIndexes)
		throws SQLException
	{
		addWarning("executeLargeUpdate: generated keys for fixed set of columns not supported", "01M18");
		return executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
	}

	/**
	 * Executes the given SQL statement and signals the driver that the
	 * auto-generated keys indicated in the given array should be made
	 * available for retrieval. The driver will ignore the array if the
	 * SQL statement is not an INSERT statement.
	 *
	 * This method should be used when the returned row count may exceed Integer.MAX_VALUE.
	 * Note: This method cannot be called on a PreparedStatement or CallableStatement.
	 * The default implementation will throw SQLFeatureNotSupportedException
	 *
	 * MonetDB only supports returing the generated key for one column,
	 * which will be the first column that has a serial.  Hence, this
	 * method cannot work as required and the driver will fall back to
	 * executing with request to the database to return the generated
	 * key, if any.
	 *
	 * @param sql an SQL Data Manipulation Language (DML) statement, such as
	 *	INSERT, UPDATE or DELETE; or an SQL statement that returns nothing,
	 *	such as a DDL statement.
	 * @param columnNames an array of the names of the columns that
	 *        should be returned from the inserted row
	 * @return either (1) the row count for SQL Data Manipulation Language (DML) statements
	 *	or (2) 0 for SQL statements that return nothing
	 * @throws SQLException if a database access error occurs, this method is
	 *	called on a closed Statement, the given SQL statement produces a
	 *	ResultSet object, the second argument supplied to this method is
	 *	not a String array whose elements are valid column names,
	 *	the method is called on a PreparedStatement or CallableStatement
	 */
	@Override
	public long executeLargeUpdate(final String sql, final String[] columnNames)
		throws SQLException
	{
		addWarning("executeLargeUpdate: generated keys for fixed set of columns not supported", "01M18");
		return executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS);
	}

	//== end methods of interface Statement


	//== internal helper methods which do not belong to the JDBC interface

	/**
	 * Adds a warning to the pile of warnings this Statement object has. If
	 * there were no warnings (or clearWarnings was called) this warning will
	 * be the first, otherwise this warning will get appended to the current
	 * warning.
	 *
	 * @param reason the warning message
	 */
	private void addWarning(final String reason, final String sqlstate) {
		SQLWarning warng = new SQLWarning(reason, sqlstate);
		if (warnings == null) {
			warnings = warng;
		} else {
			warnings.setNextWarning(warng);
		}
	}

	/**
	 * Closes this Statement if there are no more open ResultSets
	 * (Responses).  Called by MonetResultSet.close().
	 */
	void closeIfCompletion() {
		if (!closeOnCompletion || lastResponseList == null)
			return;
		if (!lastResponseList.hasUnclosedResponses())
			close();
	}
}

/**
 * This internal subclass is not intended for normal use. Therefore it is restricted to
 * classes from the very same package only.
 *
 * Known issues with this class: some methods of the ResultSetMetaData object (obtained via getMetaData())
 * require that its statement argument (accessed via getStatement()) has a valid Statement object set.
 * Instances of this subclass do not have a valid Statement (see special constructor), so
 * those metadata methods do not return the correct values.
 * Special checks are programmed to prevent NullPointerExceptions, see above.
 *
 * As of Jun2016 this class is only used by MonetStatement.getGeneratedKeys()
 * Note: to resolve a javac -Xlint warning, this class definition is moved to this file.
 *
 * TODO: try to eliminate the need for this class completely.
 */
final class MonetVirtualResultSet extends MonetResultSet {
	private String results[][];
	private boolean closed;

	MonetVirtualResultSet(
		final Statement statement,
		final String[] columns,
		final String[] types,
		final String[][] results
	) throws IllegalArgumentException {
		super(statement, columns, types, results.length);
		this.results = results;
		closed = false;
	}

	/**
	 * This method is overridden in order to let it use the results array
	 * instead of the cache in the Statement object that created it.
	 *
	 * @param row the number of the row to which the cursor should move. A
	 *        positive number indicates the row number counting from the
	 *        beginning of the result set; a negative number indicates the row
	 *        number counting from the end of the result set
	 * @return true if the cursor is on the result set; false otherwise
	 * @throws SQLException if a database error occurs
	 */
	@Override
	public boolean absolute(int row) throws SQLException {
		if (closed)
			throw new SQLException("ResultSet is closed!", "M1M20");

		// first calculate what the JDBC row is
		if (row < 0) {
			// calculate the negatives...
			row = (int) tupleCount + row + 1;
		}
		// now place the row not farther than just before or after the result
		if (row < 0)
			row = 0;	// before first
		else if (row > tupleCount + 1)
			row = (int) tupleCount + 1;	// after last

		// store it
		curRow = row;

		// see if we have the row
		if (row < 1 || row > tupleCount)
			return false;

		for (int i = 0; i < results[row - 1].length; i++) {
			tlp.values[i] = results[row - 1][i];
		}

		return true;
	}

	/**
	 * Mainly here to prevent errors when the close method is called. There
	 * is no real need for this object to close it. We simply remove our
	 * resultset data.
	 */
	@Override
	public void close() {
		results = null;
		closed = true;
		super.close();
	}
}