view src/main/java/org/monetdb/jdbc/MonetStatement.java @ 970:f90d811e97eb default tip

Adjust getTableTypes() test for new table type: LOCAL TEMPORARY VIEW, added in 11.53.4 (Mar2025-SP1)
author Martin van Dinther <martin.van.dinther@monetdbsolutions.com>
date Thu, 03 Apr 2025 15:01:33 +0200 (4 days ago)
parents 5cc071c5c170
children
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.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 commands 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://github.com/MonetDB/MonetDB/issues/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 returning 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 returning 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 Math.max(getUpdateCount(), 0);
	}

	/**
	 * 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 Math.max(getUpdateCount(), 0);
	}

	/**
	 * 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 returning 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 returning 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 0x7ffffffe;	// 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 uninitialized 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 does not implement scanning and conditional
	 * removal of escape sequences. Newer MonetDB servers (post 11.45)
	 * have the capability to parse and handle JDBC/ODBC escape sequences
	 * but you can not disable it.
	 *
	 * @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 {
		// MonetDB releases Sep2022 (11.45) and older do not support JDBC escape processing in the server or driver
		if ((connection.getDatabaseMajorVersion() == 11) && (connection.getDatabaseMinorVersion() <= 45)) {
			if (enable)
				addWarning("setEscapeProcessing(true): JDBC escape syntax is not supported by this driver or server", "01M22");
		} else {
			// For newer servers (post 11.45) it is not possible to turn it off
			if (! enable)
				addWarning("setEscapeProcessing(false): Cannot disable JDBC escape processing.", "M1M05");
		}
	}

	/**
	 * 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 application 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) {
				final long updCnt = ((MonetConnection.UpdateResponse)header).count;
				if (updCnt >= 0)
					return updCnt;
			}
		}
		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 {
					counts[offset] = (count >= 0) ? count : SUCCESS_NO_INFO;
				}
				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 Math.max(getLargeUpdateCount(), 0L);
	}

	/**
	 * 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 Math.max(getLargeUpdateCount(), 0L);
	}

	/**
	 * 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 returning 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 returning 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
	 * @param sqlstate the SQLState code (5 characters)
	 */
	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;

		System.arraycopy(results[row - 1], 0, tlp.values, 0, results[row - 1].length);

		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();
	}
}