// jTDS JDBC Driver for Microsoft SQL Server and Sybase // Copyright (C) 2004 The jTDS Project // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA package net.sourceforge.jtds.jdbc; import java.sql.BatchUpdateException; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.SQLWarning; import java.sql.Types; import java.util.ArrayList; import java.util.LinkedList; import java.util.concurrent.atomic.AtomicInteger; /** * jTDS implementation of the java.sql.Statement interface.<p> * NB. As allowed by the JDBC standard and like most other drivers, * this implementation only allows one open result set at a time. * <p> * Implementation notes: * <p> * I experimented with allowing multiple open result sets as supported * by the original jTDS but rejected this approach for the following * reasons: * <ol> * <li>It is more difficult to ensure that there are no memory leaks and that * cursors are closed if multiple open sets are allowed. * <li>The use of one result set allows cursor and non cursor result sets to * be derived from exeuteQuery() or execute() and getResultSet() in the * same way that other drivers do. * </ol> * In the event of an IO failure the setClosed() method forces this statement * and associated result set to close preventing the propagation of errors. * This class includes a finalize method which increases the chances of the * statement being closed tidly in a pooled environment where the user has * forgotten to explicitly close the statement before it goes out of scope. * * @see * java.sql.Statement * * @see * java.sql.Connection#createStatement() * * @see * java.sql.ResultSet * * @author * Mike Hutchinson, Holger Rehn */ public class JtdsStatement implements java.sql.Statement { /** * Column name to be used for retrieving generated keys from the server: * "_JTDS_GENE_RATED_KEYS_" */ static final String GENKEYCOL = "_JTDS_GENE_R_ATED_KEYS_"; /* * Constants used for backwards compatibility with JDK 1.3 */ static final int RETURN_GENERATED_KEYS = 1; static final int NO_GENERATED_KEYS = 2; static final int CLOSE_CURRENT_RESULT = 1; static final int KEEP_CURRENT_RESULT = 2; static final int CLOSE_ALL_RESULTS = 3; static final int BOOLEAN = 16; static final int DATALINK = 70; static final Integer SUCCESS_NO_INFO = new Integer(-2); static final Integer EXECUTE_FAILED = new Integer(-3); static final int DEFAULT_FETCH_SIZE = 100; /** The connection owning this statement object. */ protected JtdsConnection connection; /** The TDS object used for server access. */ protected TdsCore tds; /** The read query timeout in seconds */ protected int queryTimeout; /** The current <code>ResultSet</code>. */ protected JtdsResultSet currentResult; /** The current update count. */ private int updateCount = -1; /** The fetch direction for result sets. */ protected int fetchDirection = ResultSet.FETCH_FORWARD; /** The type of result sets created by this statement. */ protected int resultSetType = ResultSet.TYPE_FORWARD_ONLY; /** The concurrency of result sets created by this statement. */ protected int resultSetConcurrency = ResultSet.CONCUR_READ_ONLY; /** The fetch size (default 100, only used by cursor * <code>ResultSet</code>s). */ protected int fetchSize = DEFAULT_FETCH_SIZE; /** The cursor name to be used for positioned updates. */ protected String cursorName; /** The maximum field size (not used at present). */ protected int maxFieldSize; /** The maximum number of rows to return (not used at present). */ protected int maxRows; /** True if SQL statements should be preprocessed. */ protected boolean escapeProcessing = true; /** SQL Diagnostic exceptions and warnings. */ protected final SQLDiagnostic messages; /** Batched SQL Statement array. */ protected ArrayList batchValues; /** Dummy result set for getGeneratedKeys. */ protected CachedResultSet genKeyResultSet; /** * List of queued results (update counts, possibly followed by a * <code>ResultSet</code>). */ protected final LinkedList resultQueue = new LinkedList(); /** List of open result sets. */ protected ArrayList openResultSets; /** The cached column meta data. */ protected ColInfo[] colMetaData; /** * <table> * <tr> * <td>0</td> * <td>- this statement is open</td> * </tr> * <tr> * <td>1</td> * <td>- this statement is currently being closed</td> * </tr> * <tr> * <td>2</td> * <td>- this statement is closed</td> * </tr> * </table> */ private final AtomicInteger _Closed = new AtomicInteger(); /** * Construct a new Statement object. * * @param connection The parent connection. * @param resultSetType The result set type for example TYPE_FORWARD_ONLY. * @param resultSetConcurrency The concurrency for example CONCUR_READ_ONLY. */ JtdsStatement(JtdsConnection connection, int resultSetType, int resultSetConcurrency) throws SQLException { // // This is a good point to do common validation of the result set type // if (resultSetType < ResultSet.TYPE_FORWARD_ONLY || resultSetType > ResultSet.TYPE_SCROLL_SENSITIVE + 1) { String method; if (this instanceof JtdsCallableStatement) { method = "prepareCall"; } else if (this instanceof JtdsPreparedStatement) { method = "prepareStatement"; } else { method = "createStatement"; } throw new SQLException( Messages.get("error.generic.badparam", "resultSetType", method), "HY092"); } // // Ditto for the result set concurrency // if (resultSetConcurrency < ResultSet.CONCUR_READ_ONLY || resultSetConcurrency > ResultSet.CONCUR_UPDATABLE + 2) { String method; if (this instanceof JtdsCallableStatement) { method = "prepareCall"; } else if (this instanceof JtdsPreparedStatement) { method = "prepareStatement"; } else { method = "createStatement"; } throw new SQLException( Messages.get("error.generic.badparam", "resultSetConcurrency", method), "HY092"); } this.connection = connection; this.resultSetType = resultSetType; this.resultSetConcurrency = resultSetConcurrency; tds = connection.getCachedTds(); if (tds == null) { messages = new SQLDiagnostic(connection.getServerType()); tds = new TdsCore(this.connection, messages); } else { messages = tds.getMessages(); } } /** * Called when this object goes out of scope to close any * <code>ResultSet</code> object and this statement. */ protected void finalize() throws Throwable { super.finalize(); try { close(); } catch (SQLException e) { // Ignore errors } } /** * Get the Statement's TDS object. * * @return The TDS support as a <code>TdsCore</core> Object. */ TdsCore getTds() { return tds; } /** * Get the statement's warnings list. * * @return The warnings list as a <code>SQLDiagnostic</code>. */ SQLDiagnostic getMessages() { return messages; } /** * Check that this statement is still open. * * @throws SQLException if statement closed. */ protected void checkOpen() throws SQLException { if (isClosed()) throw new SQLException( Messages.get("error.generic.closed", "Statement"), "HY010"); } /** * Check that the exception is caused by the failure to open a * cursor and not by a more serious SQL error. * * @param e the exception returned by the cursor class * @throws SQLException if exception is not due to a cursor error */ protected void checkCursorException(SQLException e) throws SQLException{ if (connection == null || connection.isClosed() || "HYT00".equals(e.getSQLState()) || "HY008".equals(e.getSQLState())) { // Serious error or timeout so return exception to caller throw e; } if (connection.getServerType() == Driver.SYBASE) { // Allow retry for Sybase return; } // // Check cursor specific errors and ranges for SQL Server // int error = e.getErrorCode(); if (error >= 16900 && error <= 16999) { // Errors in this range are all related to the cursor API. // This is true for all versions of SQL Server. return; } if (error == 6819) { // A FOR XML clause was found return; } if (error == 8654) { // A inrow textptr exists return; } if (error == 8162) { // Formal parameter '%.*ls' was defined as OUTPUT but the actual // parameter not declared OUTPUT. This happens when trying to // execute a stored procedure with output parameters via a cursor. return; } // // More serious error we should rethrow the error and // not allow the driver to re-execute sql. // throw e; } /** * Report that user tried to call a method which has not been implemented. * * @param method The method name to report in the error message. * @throws SQLException */ static void notImplemented(String method) throws SQLException { throw new SQLException( Messages.get("error.generic.notimp", method), "HYC00"); } /** * Close current result set (if any). */ void closeCurrentResultSet() throws SQLException { try { if (currentResult != null) { currentResult.close(); } // } catch (SQLException e) { // Ignore } finally { currentResult = null; } } /** * Close all result sets. */ void closeAllResultSets() throws SQLException { try { if (openResultSets != null) { for (int i = 0; i < openResultSets.size(); i++) { JtdsResultSet rs = (JtdsResultSet) openResultSets.get(i); if (rs != null) { rs.close(); } } } closeCurrentResultSet(); } finally { openResultSets = null; } } /** * Add an SQLWarning object to the statement warnings list. * * @param w The SQLWarning to add. */ void addWarning(SQLWarning w) { messages.addWarning(w); } /** * Execute the SQL batch on a MS server. * * @param size the total size of the batch * @param executeSize the maximum number of statements to send in one request * @param counts the returned update counts * @return chained exceptions linked to a <code>SQLException</code> * @throws SQLException if a serious error occurs during execution */ protected SQLException executeMSBatch(int size, int executeSize, ArrayList counts) throws SQLException { SQLException sqlEx = null; for (int i = 0; i < size;) { Object value = batchValues.get(i); ++i; // Execute batch now if max size reached or end of batch boolean executeNow = (i % executeSize == 0) || i == size; tds.startBatch(); tds.executeSQL((String) value, null, null, false, 0, -1, -1, executeNow); // If the batch has been sent, process the results if (executeNow) { sqlEx = tds.getBatchCounts(counts, sqlEx); // If a serious error then we stop execution now as count // is too small. if (sqlEx != null && counts.size() != i) { break; } } } return sqlEx; } /** * Execute the SQL batch on a Sybase server. * <p/> * Sybase needs to have the SQL concatenated into one TDS language packet. This method will be overriden for * <code>PreparedStatements</code>. * * @param size the total size of the batch * @param executeSize the maximum number of statements to send in one request * @param counts the returned update counts * @return chained exceptions linked to a <code>SQLException</code> * @throws SQLException if a serious error occurs during execution */ protected SQLException executeSybaseBatch(int size, int executeSize, ArrayList counts) throws SQLException { StringBuilder sql = new StringBuilder(size * 32); // Make buffer reasonable size SQLException sqlEx = null; for (int i = 0; i < size;) { Object value = batchValues.get(i); ++i; // Execute batch now if max size reached or end of batch boolean executeNow = (i % executeSize == 0) || i == size; sql.append((String) value).append(' '); if (executeNow) { tds.executeSQL(sql.toString(), null, null, false, 0, -1, -1, true); sql.setLength(0); // If the batch has been sent, process the results sqlEx = tds.getBatchCounts(counts, sqlEx); // If a serious error or a server error then we stop // execution now as count is too small. if (sqlEx != null && counts.size() != i) { break; } } } return sqlEx; } /** * Executes SQL to obtain a result set. * * @param sql the SQL statement to execute * @param spName optional stored procedure name * @param params optional parameters * @param useCursor whether a cursor should be created for the SQL * @return the result set generated by the query */ protected ResultSet executeSQLQuery(String sql, String spName, ParamInfo[] params, boolean useCursor) throws SQLException { String warningMessage = null; // // Try to open a cursor result set if required // if (useCursor) { try { if (connection.getServerType() == Driver.SQLSERVER) { currentResult = new MSCursorResultSet(this, sql, spName, params, resultSetType, resultSetConcurrency); return currentResult; } else { // Use client side cursor for Sybase currentResult = new CachedResultSet(this, sql, spName, params, resultSetType, resultSetConcurrency); return currentResult; } } catch (SQLException e) { checkCursorException(e); warningMessage = '[' + e.getSQLState() + "] " + e.getMessage(); } } // // Could not open a cursor (or was not requested) so try a direct select // if (spName != null && connection.getUseMetadataCache() && connection.getPrepareSql() == TdsCore.PREPARE && colMetaData != null && connection.getServerType() == Driver.SQLSERVER) { // There is cached meta data available for this // prepared statement tds.setColumns(colMetaData); tds.executeSQL(sql, spName, params, true, queryTimeout, maxRows, maxFieldSize, true); } else { tds.executeSQL(sql, spName, params, false, queryTimeout, maxRows, maxFieldSize, true); } // Update warning chain if cursor was downgraded before processing results if (warningMessage != null) { addWarning(new SQLWarning( Messages.get("warning.cursordowngraded", warningMessage), "01000")); } // Ignore update counts preceding the result set. All drivers seem to // do this. while (!tds.getMoreResults() && !tds.isEndOfResponse()); // check for server side errors messages.checkErrors(); if (tds.isResultSet()) { currentResult = new JtdsResultSet(this, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, tds.getColumns()); } else { throw new SQLException( Messages.get("error.statement.noresult"), "24000"); } return currentResult; } /** * Executes any type of SQL. * * @param sql the SQL statement to execute * @param spName optional stored procedure name * @param params optional parameters * @param update whether the caller is {@link #executeUpdate} * @param useCursor whether the requested result set type or concurrency * or connection properties request usage of a cursor * @return <code>true</code> if the first result is a result set * @throws SQLException if an error condition occurs */ protected boolean executeSQL(String sql, String spName, ParamInfo[] params, boolean update, boolean useCursor) throws SQLException { String warningMessage = null; // // For SQL Server, try to open a cursor result set if required // (and possible). // if (connection.getServerType() == Driver.SQLSERVER && !update && useCursor) { try { currentResult = new MSCursorResultSet(this, sql, spName, params, resultSetType, resultSetConcurrency); return true; } catch (SQLException e) { checkCursorException(e); warningMessage = '[' + e.getSQLState() + "] " + e.getMessage(); } } // // We are talking to a Sybase server or we could not open a cursor // or we did not have a SELECT so just execute the SQL normally. // tds.executeSQL(sql, spName, params, false, queryTimeout, maxRows, maxFieldSize, true); if (warningMessage != null) { // Update warning chain if cursor was downgraded addWarning(new SQLWarning(Messages.get( "warning.cursordowngraded", warningMessage), "01000")); } if (processResults(update)) { Object nextResult = resultQueue.removeFirst(); // Next result is an update count if (nextResult instanceof Integer) { updateCount = ((Integer) nextResult).intValue(); return false; } // Next result is a ResultSet. Set currentResult and remove it. currentResult = (JtdsResultSet) nextResult; return true; } else { return false; } } /** * Queue up update counts into {@link #resultQueue} until the end of the * response is reached or a <code>ResultSet</code> is encountered. Calling * <code>processResults</code> while a <code>ResultSet</code> is open will * not close it, but will consume all remaining rows. * * @param update * <code>true</code> if the method is called from within * <code>executeUpdate</code> * * @return * <code>true</code> if there are any results, <code>false</code> otherwise * * @throws SQLException * if an error condition occurs */ private boolean processResults( boolean update ) throws SQLException { if( ! resultQueue.isEmpty() ) throw new IllegalStateException( "There should be no queued results." ); while( !tds.isEndOfResponse() ) { if( !tds.getMoreResults() ) { if( tds.isUpdateCount() ) { if( update && connection.getLastUpdateCount() ) { resultQueue.clear(); } resultQueue.addLast( new Integer( tds.getUpdateCount() ) ); } } else { // get column layout of the resultset ColInfo[] columns = tds.getColumns(); // ensure this is the generated key by checking column layout if( columns.length == 1 && columns[0].name.equals( GENKEYCOL ) ) { // fix column name columns[0].name = "ID"; // drop previously received generated keys genKeyResultSet = null; /* * FIXME: If there already was a ResultSet holding generated keys * we should just add an additional row instead, but this is only * possible if we are able to buffer rows to disk to prevent high * memory consumption. * * Note: This would only apply if column layout is identical. */ // add all generated keys in the resultset (possible memory leak) while( tds.getNextRow() ) { if( genKeyResultSet == null ) { // start new ResultSet, containing the first generated key genKeyResultSet = new CachedResultSet( this, tds.getColumns(), tds.getRowData() ); } else { // add additional generated key genKeyResultSet.addRow( tds.getRowData() ); } } } else { if( update && resultQueue.isEmpty() ) { // throw exception but queue up any previous ones SQLException ex = new SQLException( Messages.get( "error.statement.nocount" ), "07000" ); ex.setNextException( messages.exceptions ); throw ex; } // this also clears computed data in TdsCore Object[] computed = tds.getComputedRowData(); if( computed != null ) { // create computed result set resultQueue.add( new CachedResultSet( this, tds.getComputedColumns(), computed ) ); } else { resultQueue.add( new JtdsResultSet( this, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, tds.getColumns() ) ); } break; } } } // Check for server side errors getMessages().checkErrors(); return !resultQueue.isEmpty(); } /** * Cache as many results as possible (up to the first * <code>ResultSet</code>). Called by <code>ResultSet</code>s when the * end is reached. */ protected void cacheResults() throws SQLException { // Cache results processResults(false); } /** * Resets the <code>Statement</code>, by cleaning up all queued and * unprocessed results. Called by all execute methods and {@link #close()} * * @throws SQLException * if an error occurs */ protected void reset() throws SQLException { updateCount = -1; resultQueue.clear(); genKeyResultSet = null; // consume all response tokens // REVIEW: shouldn't we issue a cancel first to stop the server from sending more data? tds.clearResponseQueue(); // don't throw old exceptions, they belong to a previous execution messages.clearWarnings(); messages.exceptions = null; closeAllResultSets(); } /** * Implements the common functionality for plain statement {@link #execute} * and {#link #executeUpdate}: basic checks, cleaning up of previous * results, setting up and executing the query and loading the first * results. * * @param sql an SQL <code>INSERT</code>, <code>UPDATE</code> or * <code>DELETE</code> statement or an SQL statement that * returns nothing, such as an SQL DDL statement * @param autoGeneratedKeys a flag indicating whether auto-generated keys * should be made available for retrieval * @param update boolean flag indicating whether the caller is * {@link #executeUpdate} -- in this case an exception is * thrown if the first result is not an update count and no * cursor is created (direct execution) * @return <code>true</code> if the first result is a * <code>ResultSet</code>, <code>false</code> if it's an update * count * @see #execute * @see #executeUpdate */ private boolean executeImpl(String sql, int autoGeneratedKeys, boolean update) throws SQLException { reset(); if (sql == null || sql.length() == 0) { throw new SQLException(Messages.get("error.generic.nosql"), "HY000"); } String sqlWord = ""; if (escapeProcessing) { String tmp[] = SQLParser.parse(sql, null, connection, false); if (tmp[1].length() != 0) { throw new SQLException( Messages.get("error.statement.badsql"), "07000"); } sql = tmp[0]; sqlWord = tmp[2]; } else { // Escape processing turned off so // see if we can extract "insert" from start of statement sql = sql.trim(); if (sql.length() > 5) { sqlWord = sql.substring(0,6).toLowerCase(); } } boolean returnKeys; if (autoGeneratedKeys == RETURN_GENERATED_KEYS) { // REVIEW: how would this fail and why? // // this will fail if we execute multiple statements at once and the first isn't an INSERT // returnKeys = "insert".equals(sqlWord); returnKeys = true; } else if (autoGeneratedKeys == NO_GENERATED_KEYS) { returnKeys = false; } else { throw new SQLException( Messages.get("error.generic.badoption", Integer.toString(autoGeneratedKeys), "autoGeneratedKeys"), "HY092"); } if (returnKeys) { if (connection.getServerType() == Driver.SQLSERVER && connection.getDatabaseMajorVersion() >= 8) { sql += " SELECT SCOPE_IDENTITY() AS " + GENKEYCOL; } else { sql += " SELECT @@IDENTITY AS " + GENKEYCOL; } } return executeSQL(sql, null, null, update, !update && useCursor(returnKeys, sqlWord)); } /** * Determines whether a cursor should be used based on the requested result * set type and concurrency, whether a cursor name has been set, the * <code>useCursors</code> connection property has been set, the first * word in the SQL query is either SELECT or EXEC/EXECUTE and no generated * keys are returned. * * @param returnKeys indicates whether keys will be returned by the query * @param sqlWord the first word in the SQL query; can be * <code>null</code> if the caller is * {@link #executeQuery} * @return <code>true</code> if a cursor should be used, <code>false</code> * if not */ protected boolean useCursor(boolean returnKeys, String sqlWord) { return (resultSetType != ResultSet.TYPE_FORWARD_ONLY || resultSetConcurrency != ResultSet.CONCUR_READ_ONLY || connection.getUseCursors() || cursorName != null) && !returnKeys && (sqlWord == null || "select".equals(sqlWord) || sqlWord.startsWith("exec")); } /** * Retrieve the default fetch size for this statement. * * @return the default fetch size for a new <code>ResultSet</code> */ int getDefaultFetchSize() { return (0 < maxRows && maxRows < DEFAULT_FETCH_SIZE) ? maxRows : DEFAULT_FETCH_SIZE; } // ------------------ java.sql.Statement methods ---------------------- public int getFetchDirection() throws SQLException { checkOpen(); return fetchDirection; } public int getFetchSize() throws SQLException { checkOpen(); return fetchSize; } public int getMaxFieldSize() throws SQLException { checkOpen(); return maxFieldSize; } public int getMaxRows() throws SQLException { checkOpen(); return maxRows; } public int getQueryTimeout() throws SQLException { checkOpen(); return queryTimeout; } public int getResultSetConcurrency() throws SQLException { checkOpen(); return resultSetConcurrency; } public int getResultSetHoldability() throws SQLException { checkOpen(); return JtdsResultSet.HOLD_CURSORS_OVER_COMMIT; } public int getResultSetType() throws SQLException { checkOpen(); return resultSetType; } public int getUpdateCount() throws SQLException { checkOpen(); return updateCount; } public void cancel() throws SQLException { checkOpen(); if (tds != null) { tds.cancel(false); } } public void clearBatch() throws SQLException { checkOpen(); if (batchValues != null) { batchValues.clear(); } } public void clearWarnings() throws SQLException { checkOpen(); messages.clearWarnings(); } @Override public void close() throws SQLException { // set to 'now closing' if( _Closed.compareAndSet( 0, 1 ) ) { SQLException releaseEx = null; try { /* The re-initialization has been added in revision [1196], replacing * most of the error handling done before. * * The problem is, that in releaseTds() we completely consume the * response queue, including error tokens produced in response to a * previous statement. But we don't want to throw an exception from * close() just because a previous statement failed, as in: * * st.executeUpdate( "create table #Bug559 (A int, unique (A))" ); * try * { * st.executeUpdate( "select 1;insert into #Bug559 values(1);insert into #Bug559 values(1)" ); * fail(); * } * catch( SQLException e ) * { * // expected, executeUpdate() cannot return a resultset * } * st.close(); // <- unique constraint violation error before [1196] * * Here, the second insert fails because of a unique constraint * violation. But the driver will already have aborted execution * because the first statement returns a resultset which is not * allowed when calling executeUpdate(), so close() would then throw * the unique constraint violation error. * * Since we are closing the Statement, all we care about are errors * that are directly related to closing, not previous SQL statements. */ // drop previous errors and warnings and consume all response tokens reset(); try { if( ! connection.isClosed() ) { connection.releaseTds( tds ); } // check for server side errors tds.getMessages().checkErrors(); } catch( SQLException ex ) { // remember any exception thrown releaseEx = ex; } finally { // set closed flag, closeAllResultSets() still required statement // to be 'open' _Closed.set( 2 ); // clean up everything tds = null; connection.removeStatement( this ); connection = null; } } finally { // re-throw statement close exception if( releaseEx != null ) { throw releaseEx; } } } } public boolean getMoreResults() throws SQLException { checkOpen(); return getMoreResults(CLOSE_ALL_RESULTS); } /** * Execute batch of SQL Statements. * <p/> * The JDBC3 standard says that the behavior of this method must be * consistent for any DBMS. As Sybase (and to a lesser extent SQL Server) * will sometimes continue after a batch execution error, the only way to * comply with the standard is to always return an array of update counts * the same size as the batch list. Slots in the array beyond the last * executed statement are set to <code>EXECUTE_FAILED</code>. <p/> * * There is a problem with certain statements, returning more update counts * than there are batch operations. (see bug []) * * @return update counts as an <code>int[]</code> */ public int[] executeBatch() throws SQLException, BatchUpdateException { checkOpen(); reset(); if (batchValues == null || batchValues.size() == 0) { return new int[0]; } int size = batchValues.size(); int executeSize = connection.getBatchSize(); executeSize = (executeSize == 0) ? Integer.MAX_VALUE : executeSize; SQLException sqlEx; ArrayList counts = new ArrayList(size); try { // Lock the connection, making sure the batch executes atomically. This is especially important in the // case of prepared statement batches (where we don't want the prepares rolled back before being executed) // but should also provide some level of sanity in the general case. synchronized (connection) { if (connection.getServerType() == Driver.SYBASE && connection.getTdsVersion() == Driver.TDS50) { sqlEx = executeSybaseBatch(size, executeSize, counts); } else { sqlEx = executeMSBatch(size, executeSize, counts); } } // Ensure array is the same size as the original statement list int updateCounts[] = new int[size]; // Copy the update counts into the int array int results = counts.size(); for (int i = 0; i < results && i < size; i++) { updateCounts[i] = ((Integer) counts.get(i)).intValue(); } // Pad any remaining slots with EXECUTE_FAILED for (int i = results; i < updateCounts.length; i++) { updateCounts[i] = EXECUTE_FAILED.intValue(); } // See if we should return an exception if (sqlEx != null) { BatchUpdateException batchEx = new BatchUpdateException(sqlEx.getMessage(), sqlEx.getSQLState(), sqlEx.getErrorCode(), updateCounts); // Chain any other exceptions batchEx.setNextException(sqlEx.getNextException()); throw batchEx; } return updateCounts; } catch (BatchUpdateException ex) { // If it's a BatchUpdateException let it go throw ex; } catch (SQLException ex) { // An SQLException can only occur while sending the batch // (getBatchCounts() doesn't throw SQLExceptions), so we have to // end the batch and return the partial results // FIXME What should we send here to flush out the batch? // Come to think of it, is there any circumstance under which this // could actually happen without the connection getting closed? // No counts will have been returned either as last packet will not // have been sent. throw new BatchUpdateException(ex.getMessage(), ex.getSQLState(), ex.getErrorCode(), new int[0]); } finally { clearBatch(); } } public void setFetchDirection(int direction) throws SQLException { checkOpen(); switch (direction) { case ResultSet.FETCH_UNKNOWN: case ResultSet.FETCH_REVERSE: case ResultSet.FETCH_FORWARD: fetchDirection = direction; break; default: throw new SQLException( Messages.get("error.generic.badoption", Integer.toString(direction), "direction"), "24000"); } } public void setFetchSize(int rows) throws SQLException { checkOpen(); if (rows < 0) { throw new SQLException( Messages.get("error.generic.optltzero", "setFetchSize"), "HY092"); } else if (maxRows > 0 && rows > maxRows) { throw new SQLException( Messages.get("error.statement.gtmaxrows"), "HY092"); } if (rows == 0) { rows = getDefaultFetchSize(); } fetchSize = rows; } public void setMaxFieldSize(int max) throws SQLException { checkOpen(); if (max < 0) { throw new SQLException( Messages.get("error.generic.optltzero", "setMaxFieldSize"), "HY092"); } maxFieldSize = max; } public void setMaxRows(int max) throws SQLException { checkOpen(); if (max < 0) { throw new SQLException( Messages.get("error.generic.optltzero", "setMaxRows"), "HY092"); } if (max > 0 && max < fetchSize) { // Just for consistency with setFetchSize() fetchSize = max; } maxRows = max; } public void setQueryTimeout(int seconds) throws SQLException { checkOpen(); if (seconds < 0) { throw new SQLException( Messages.get("error.generic.optltzero", "setQueryTimeout"), "HY092"); } queryTimeout = seconds; } public boolean getMoreResults(int current) throws SQLException { checkOpen(); switch (current) { case CLOSE_ALL_RESULTS: updateCount = -1; closeAllResultSets(); break; case CLOSE_CURRENT_RESULT: updateCount = -1; closeCurrentResultSet(); break; case KEEP_CURRENT_RESULT: updateCount = -1; // If there is an open result set it is transferred to // the list of open result sets. For JtdsResultSet // result sets we cache the remaining data. For CachedResultSet // result sets the data is already cached. if (openResultSets == null) { openResultSets = new ArrayList(); } if (currentResult instanceof MSCursorResultSet || currentResult instanceof CachedResultSet) { // NB. Due to restrictions on the way API cursors are // created, MSCursorResultSet can never be followed by // any other result sets, update counts or return variables. openResultSets.add(currentResult); } else if (currentResult != null) { currentResult.cacheResultSetRows(); openResultSets.add(currentResult); } currentResult = null; break; default: throw new SQLException( Messages.get("error.generic.badoption", Integer.toString(current), "current"), "HY092"); } // Check for server side errors messages.checkErrors(); // Dequeue any results if (!resultQueue.isEmpty() || processResults(false)) { Object nextResult = resultQueue.removeFirst(); // Next result is an update count if (nextResult instanceof Integer) { updateCount = ((Integer) nextResult).intValue(); return false; } // Next result is a ResultSet. Set currentResult and remove it. currentResult = (JtdsResultSet) nextResult; return true; } else { return false; } } public void setEscapeProcessing(boolean enable) throws SQLException { checkOpen(); escapeProcessing = enable; } public int executeUpdate(String sql) throws SQLException { return executeUpdate(sql, NO_GENERATED_KEYS); } public void addBatch(String sql) throws SQLException { checkOpen(); if (sql == null) { throw new NullPointerException(); } if (batchValues == null) { batchValues = new ArrayList(); } if (escapeProcessing) { String tmp[] = SQLParser.parse(sql, null, connection, false); if (tmp[1].length() != 0) { throw new SQLException( Messages.get("error.statement.badsql"), "07000"); } sql = tmp[0]; } batchValues.add(sql); } public void setCursorName(String name) throws SQLException { checkOpen(); cursorName = name; if (name != null) { // Reset statement type to JDBC 1 default. resultSetType = ResultSet.TYPE_FORWARD_ONLY; fetchSize = 1; // Needed for positioned updates } } public boolean execute(String sql) throws SQLException { checkOpen(); return executeImpl(sql, NO_GENERATED_KEYS, false); } public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { checkOpen(); executeImpl(sql, autoGeneratedKeys, true); int res = getUpdateCount(); return res == -1 ? 0 : res; } public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { checkOpen(); return executeImpl(sql, autoGeneratedKeys, false); } public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { checkOpen(); if (columnIndexes == null) { throw new SQLException( Messages.get("error.generic.nullparam", "executeUpdate"),"HY092"); } else if (columnIndexes.length != 1) { throw new SQLException( Messages.get("error.generic.needcolindex", "executeUpdate"),"HY092"); } return executeUpdate(sql, RETURN_GENERATED_KEYS); } public boolean execute(String sql, int[] columnIndexes) throws SQLException { checkOpen(); if (columnIndexes == null) { throw new SQLException( Messages.get("error.generic.nullparam", "execute"),"HY092"); } else if (columnIndexes.length != 1) { throw new SQLException( Messages.get("error.generic.needcolindex", "execute"),"HY092"); } return executeImpl(sql, RETURN_GENERATED_KEYS, false); } @Override public Connection getConnection() throws SQLException { checkOpen(); return connection; } @Override public ResultSet getGeneratedKeys() throws SQLException { checkOpen(); if( genKeyResultSet == null ) { // create and return an empty result set genKeyResultSet = new CachedResultSet( this, new String[] { "ID" }, new int[] { Types.INTEGER } ); } // non't allow manipulation genKeyResultSet.setConcurrency( ResultSet.CONCUR_READ_ONLY ); return genKeyResultSet; } public ResultSet getResultSet() throws SQLException { checkOpen(); // if (currentResult instanceof MSCursorResultSet || currentResult instanceof CachedResultSet) { return currentResult; } // // See if we are returning a forward read only resultset // if (currentResult == null || (resultSetType == ResultSet.TYPE_FORWARD_ONLY && resultSetConcurrency == ResultSet.CONCUR_READ_ONLY)) { return currentResult; } // // OK Now create a CachedResultSet based on the existng result set. // currentResult = new CachedResultSet(currentResult, true); return currentResult; } public SQLWarning getWarnings() throws SQLException { checkOpen(); return messages.getWarnings(); } public int executeUpdate(String sql, String[] columnNames) throws SQLException { checkOpen(); if (columnNames == null) { throw new SQLException( Messages.get("error.generic.nullparam", "executeUpdate"),"HY092"); } else if (columnNames.length != 1) { throw new SQLException( Messages.get("error.generic.needcolname", "executeUpdate"),"HY092"); } return executeUpdate(sql, RETURN_GENERATED_KEYS); } public boolean execute(String sql, String[] columnNames) throws SQLException { checkOpen(); if (columnNames == null) { throw new SQLException( Messages.get("error.generic.nullparam", "execute"),"HY092"); } else if (columnNames.length != 1) { throw new SQLException( Messages.get("error.generic.needcolname", "execute"),"HY092"); } return executeImpl(sql, RETURN_GENERATED_KEYS, false); } public ResultSet executeQuery(String sql) throws SQLException { checkOpen(); reset(); if (sql == null || sql.length() == 0) { throw new SQLException(Messages.get("error.generic.nosql"), "HY000"); } if (escapeProcessing) { String tmp[] = SQLParser.parse(sql, null, connection, false); if (tmp[1].length() != 0) { throw new SQLException( Messages.get("error.statement.badsql"), "07000"); } sql = tmp[0]; } return executeSQLQuery(sql, null, null, useCursor(false, null)); } /** * @return * whether this {@link JtdsStatement} has been closed */ @Override public boolean isClosed() throws SQLException { return _Closed.get() == 2; } /* (non-Javadoc) * @see java.sql.Statement#isPoolable() */ public boolean isPoolable() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Statement#setPoolable(boolean) */ public void setPoolable(boolean poolable) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } /* (non-Javadoc) * @see java.sql.Wrapper#isWrapperFor(java.lang.Class) */ public boolean isWrapperFor(Class arg0) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } @Override public <T> T unwrap(Class<T> iface) throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } //// JDBC4.1 demarcation, do NOT put any JDBC3/4.0 code below this line //// @Override public void closeOnCompletion() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } @Override public boolean isCloseOnCompletion() throws SQLException { // TODO Auto-generated method stub throw new AbstractMethodError(); } }