All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.microsoft.sqlserver.jdbc.SQLServerStatement Maven / Gradle / Ivy

There is a newer version: 12.8.1.jre11
Show newest version
/*
 * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
 * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
 */

package com.microsoft.sqlserver.jdbc;

import static com.microsoft.sqlserver.jdbc.SQLServerConnection.getCachedParsedSQL;
import static com.microsoft.sqlserver.jdbc.SQLServerConnection.parseAndCacheSQL;

import java.sql.BatchUpdateException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLTimeoutException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.microsoft.sqlserver.jdbc.SQLServerConnection.CityHash128Key;


/**
 * Provides an implementation of java.sql.Statement JDBC Interface to assist in creating Statements against SQL Server.
 * It also provides a number of base class implementation methods for the JDBC prepared statement and callable
 * Statements. SQLServerStatement's basic role is to execute SQL statements and return update counts and resultset rows
 * to the user application.
 *
 * Documentation for specific public methods that are undocumented can be found under Sun's standard JDBC documentation
 * for class java.sql.Statement. Those methods are part of Sun's standard JDBC documentation and therefore their
 * documentation is not duplicated here.
 * 

* Implementation Notes *

* Fetching Result sets *

* The queries first rowset is available immediately after the executeQuery. The first rs.next() does not make a server * round trip. For non server side resultsets the entire result set is in the rowset. For server side result sets the * number of rows in the rowset is set with nFetchSize *

* The API javadoc for JDBC API methods that this class implements are not repeated here. Please see Sun's JDBC API * interfaces javadoc for those details. */ public class SQLServerStatement implements ISQLServerStatement { /** * Always update serialVersionUID when prompted. */ private static final long serialVersionUID = -4421134713913331507L; final static char LEFT_CURLY_BRACKET = 123; final static char RIGHT_CURLY_BRACKET = 125; private static final String ACTIVITY_ID = " ActivityId: "; /** response buffer adaptive flag */ private boolean isResponseBufferingAdaptive = false; final boolean getIsResponseBufferingAdaptive() { return isResponseBufferingAdaptive; } /** flag if response buffering is set */ private boolean wasResponseBufferingSet = false; final boolean wasResponseBufferingSet() { return wasResponseBufferingSet; } final static String IDENTITY_QUERY = " select SCOPE_IDENTITY() AS GENERATED_KEYS"; static final String WINDOWS_KEY_STORE_NAME = "MSSQL_CERTIFICATE_STORE"; /** the stored procedure name to call (if there is one) */ String procedureName; /** OUT parameters associated with this statement's execution */ private int serverCursorId; final int getServerCursorId() { return serverCursorId; } /** server cursor row count */ private int serverCursorRowCount; final int getServerCursorRowCount() { return serverCursorRowCount; } /** The value of the poolable state */ boolean stmtPoolable; /** Streaming access to the response TDS data stream */ private TDSReader tdsReader; final TDSReader resultsReader() { return tdsReader; } final boolean wasExecuted() { return null != tdsReader; } /** * The input and out parameters for statement execution. */ transient Parameter[] inOutParam; // Parameters for prepared stmts and stored procedures /** * The statement's connection. */ final SQLServerConnection connection; /** * The user's specified query timeout (in seconds). */ int queryTimeout; /** * timeout value for canceling the query timeout */ int cancelQueryTimeoutSeconds; /** * Is closeOnCompletion is enabled? If true statement will be closed when all of its dependent result sets are * closed */ boolean isCloseOnCompletion = false; /** * Currently executing or most recently executed TDSCommand (statement cmd, server cursor cmd, ...) subject to * cancellation through Statement.cancel. * * Note: currentCommand is declared volatile to ensure that the JVM always returns the most recently set value for * currentCommand to the canceling thread. */ private volatile TDSCommand currentCommand = null; /** last statement exec command */ private TDSCommand lastStmtExecCmd = null; final void discardLastExecutionResults() { if (null != lastStmtExecCmd && !bIsClosed) { lastStmtExecCmd.close(); lastStmtExecCmd = null; } clearLastResult(); } static final java.util.logging.Logger loggerExternal = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.Statement"); /** logging class name */ final private String loggingClassName; /** trace ID */ final private String traceID; String getClassNameLogging() { return loggingClassName; } /** * Column Encryption Override. Defaults to the connection setting, in which case it will be Enabled if * columnEncryptionSetting = true in the connection setting, Disabled if false. This may also be used to set other * behavior which overrides connection level setting. */ protected SQLServerStatementColumnEncryptionSetting stmtColumnEncriptionSetting = SQLServerStatementColumnEncryptionSetting.USE_CONNECTION_SETTING; /** * Returns the statement column encryption encryption setting * * @return stmtColumnEncriptionSetting */ protected SQLServerStatementColumnEncryptionSetting getStmtColumnEncriptionSetting() { return stmtColumnEncriptionSetting; } /** * Encapsulates a subset of statement property values as they were set at execution time. */ final class ExecuteProperties { final private boolean wasResponseBufferingSet; final boolean wasResponseBufferingSet() { return wasResponseBufferingSet; } final private boolean isResponseBufferingAdaptive; final boolean isResponseBufferingAdaptive() { return isResponseBufferingAdaptive; } final private int holdability; final int getHoldability() { return holdability; } ExecuteProperties(SQLServerStatement stmt) { wasResponseBufferingSet = stmt.wasResponseBufferingSet(); isResponseBufferingAdaptive = stmt.getIsResponseBufferingAdaptive(); holdability = stmt.connection.getHoldabilityInternal(); } } /** execute properties */ private transient ExecuteProperties execProps; final ExecuteProperties getExecProps() { return execProps; } /** * Executes this Statement using TDSCommand newStmtCmd. * * The TDSCommand is assumed to be a statement execution command (StmtExecCmd, PrepStmtExecCmd, * PrepStmtBatchExecCmd). */ final void executeStatement(TDSCommand newStmtCmd) throws SQLServerException, SQLTimeoutException { // Ensure that any response left over from a previous execution has been // completely processed. There may be ENVCHANGEs in that response that // we must acknowledge before proceeding. discardLastExecutionResults(); // make sure statement hasn't been closed due to closeOnCompletion checkClosed(); execProps = new ExecuteProperties(this); try { // (Re)execute this Statement with the new command executeCommand(newStmtCmd); } catch (SQLServerException e) { if (e.getDriverErrorCode() == SQLServerException.ERROR_QUERY_TIMEOUT) { if (e.getCause() == null) { throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e); } throw new SQLTimeoutException(e.getMessage(), e.getSQLState(), e.getErrorCode(), e.getCause()); } else { throw e; } } finally { if (newStmtCmd.wasExecuted()) lastStmtExecCmd = newStmtCmd; } } /** * Executes TDSCommand newCommand through this Statement object, allowing it to be cancelled through * Statement.cancel(). * * The specified command is typically the one used to execute the statement. But it could also be a server cursor * command (fetch, move, close) generated by a ResultSet that this statement produced. * * This method does not prevent applications from simultaneously executing commands from multiple threads. The * assumption is that apps only call cancel() from another thread while the command is executing. */ final void executeCommand(TDSCommand newCommand) throws SQLServerException { // Set the new command as the current command so that // its execution can be cancelled from another thread currentCommand = newCommand; connection.executeCommand(newCommand); } /** * Flag to indicate that are potentially more results (ResultSets, update counts, or errors) to be processed in the * response. */ boolean moreResults = false; /** * The statement's current result set. */ SQLServerResultSet resultSet; /** * The number of opened result sets in the statement. */ int resultSetCount = 0; /** * Increment opened result set counter */ void incrResultSetCount() { lock.lock(); try { resultSetCount++; } finally { lock.unlock(); } } /** * Decrement opened result set counter. */ void decrResultSetCount() { lock.lock(); try { resultSetCount--; assert resultSetCount >= 0; // close statement if no more result sets opened if (isCloseOnCompletion && !(EXECUTE_BATCH == executeMethod && moreResults) && resultSetCount == 0) { closeInternal(); } } finally { lock.unlock(); } } /** * The statement execution method (executeQuery(), executeUpdate(), execute(), or executeBatch()) that was used to * execute the statement. */ static final int EXECUTE_NOT_SET = 0; static final int EXECUTE_QUERY = 1; static final int EXECUTE_UPDATE = 2; static final int EXECUTE = 3; static final int EXECUTE_BATCH = 4; static final int EXECUTE_QUERY_INTERNAL = 5; /** execute method */ int executeMethod = EXECUTE_NOT_SET; /** * The update count returned from an update. */ long updateCount = -1; /** * The status of escape processing. */ boolean escapeProcessing; /** Limit for the maximum number of rows in a ResultSet */ int maxRows = 0; // default: 0 --> no limit /** Limit for the size of data (in bytes) returned for any column value */ int maxFieldSize = 0; // default: 0 --> no limit /** * The user's specified result set concurrency. */ int resultSetConcurrency; /** * The app result set type. This is the value passed to the statement's constructor (or inferred by default) when * the statement was created. ResultSet.getType() returns this value. It may differ from the SQL Server result set * type (see below). Namely, an app result set type of TYPE_FORWARD_ONLY will have an SQL Server result set type of * TYPE_SS_DIRECT_FORWARD_ONLY or TYPE_SS_SERVER_CURSOR_FORWARD_ONLY depending on the value of the selectMethod * connection property. * * Possible values of the app result set type are: * * TYPE_FORWARD_ONLY TYPE_SCROLL_INSENSITIVE TYPE_SCROLL_SENSITIVE TYPE_SS_DIRECT_FORWARD_ONLY * TYPE_SS_SERVER_CURSOR_FORWARD_ONLY TYPE_SS_SCROLL_DYNAMIC TYPE_SS_SCROLL_KEYSET TYPE_SS_SCROLL_STATIC */ int appResultSetType; /** * The SQL Server result set type. This is the value used everywhere EXCEPT ResultSet.getType(). This value may or * may not be the same as the app result set type (above). * * Possible values of the SQL Server result set type are: * * TYPE_SS_DIRECT_FORWARD_ONLY TYPE_SS_SERVER_CURSOR_FORWARD_ONLY TYPE_SS_SCROLL_DYNAMIC TYPE_SS_SCROLL_KEYSET * TYPE_SS_SCROLL_STATIC */ int resultSetType; final int getSQLResultSetType() { return resultSetType; } final int getCursorType() { return getResultSetScrollOpt() & ~TDS.SCROLLOPT_PARAMETERIZED_STMT; } /** * Returns whether to request a server cursor when executing this statement. * * Executing a statement with execute() or executeQuery() requests a server cursor in all scrollability and * updatability combinations except direct forward-only, read-only. * * Note that when execution requests a server cursor (i.e. this method returns true), there is no guarantee that SQL * Server returns one. The variable executedSqlDirectly indicates whether SQL Server executed the query with a * cursor or not. * * @return true if statement execution requests a server cursor, false otherwise. */ final boolean isCursorable(int executeMethod) { return resultSetType != SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY && (EXECUTE == executeMethod || EXECUTE_QUERY == executeMethod); } /** * Indicates whether SQL Server executed this statement with a cursor or not. * * When trying to execute a cursor-unfriendly statement with a server cursor, SQL Server may choose to execute the * statement directly (i.e. as if no server cursor had been requested) rather than fail to execute the statement at * all. We need to know when this happens so that if no rows are returned, we can tell whether the result is an * empty result set or a cursored result set with rows to be fetched later. */ boolean executedSqlDirectly = false; /** * Indicates whether OUT parameters (cursor ID and row count) from cursorized execution of this statement are * expected in the response. * * In most cases, except for severe errors, cursor OUT parameters are returned whenever a cursor is requested for * statement execution. Even if SQL Server does not cursorize the statement as requested, these values are still * present in the response and must be processed, even though their values are meaningless in that case. */ boolean expectCursorOutParams; class StmtExecOutParamHandler extends TDSTokenHandler { SQLServerStatement statement; StmtExecOutParamHandler(SQLServerStatement statement) { super("StmtExecOutParamHandler"); this.statement = statement; } @Override boolean onRetStatus(TDSReader tdsReader) throws SQLServerException { (new StreamRetStatus()).setFromTDS(tdsReader); return true; } @Override boolean onRetValue(TDSReader tdsReader) throws SQLServerException { if (expectCursorOutParams) { Parameter param = new Parameter( Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)); // Read the cursor ID param.skipRetValStatus(tdsReader); serverCursorId = param.getInt(tdsReader, statement); param.skipValue(tdsReader, true); param = new Parameter(Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)); // Read the row count (-1 means unknown) param.skipRetValStatus(tdsReader); if (-1 == (serverCursorRowCount = param.getInt(tdsReader, statement))) serverCursorRowCount = SQLServerResultSet.UNKNOWN_ROW_COUNT; param.skipValue(tdsReader, true); // We now have everything we need to build the result set. expectCursorOutParams = false; return true; } return false; } @Override boolean onDone(TDSReader tdsReader) throws SQLServerException { return false; } } /** * The cursor name. */ String cursorName; /** * The user's specified fetch size. Only used for server side result sets. Client side cursors read all rows. */ int nFetchSize; /** * default fetch size */ int defaultFetchSize; /** * The users specified result set fetch direction */ int nFetchDirection; /** * True is the statement is closed */ boolean bIsClosed; /** * True if the user requested to driver to generate insert keys */ boolean bRequestedGeneratedKeys; /** * The result set if auto generated keys were requested. */ private transient ResultSet autoGeneratedKeys; /** * The array of objects in a batched call. Applicable to statements and prepared statements When the * iterativeBatching property is turned on. */ /** The buffer that accumulates batchable statements */ private final ArrayList batchStatementBuffer = new ArrayList<>(); /** logging init at the construction */ static final private java.util.logging.Logger stmtlogger = java.util.logging.Logger .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerStatement"); /** Returns the statement's id for logging info */ @Override public String toString() { return traceID; } /** Generate the statement's logging ID */ private static final AtomicInteger lastStatementID = new AtomicInteger(0); /* * T-SQL TOP syntax regex */ private final static Pattern TOP_SYNTAX = Pattern.compile("\\bTOP\\s*\\(\\s*\\?\\s*\\)", Pattern.CASE_INSENSITIVE); private static int nextStatementID() { return lastStatementID.incrementAndGet(); } /** * Constructs a SQLServerStatement * * @param con * The statements connections. * @param nType * The statement type. * @param nConcur * The statement concurrency. * @param stmtColEncSetting * The statement column encryption setting. * @exception SQLServerException * The statement could not be created. */ SQLServerStatement(SQLServerConnection con, int nType, int nConcur, SQLServerStatementColumnEncryptionSetting stmtColEncSetting) throws SQLServerException { // Return a string representation of this statement's unqualified class name // (e.g. "SQLServerStatement" or "SQLServerPreparedStatement"), // its unique ID, and its parent connection. int statementID = nextStatementID(); String classN = getClass().getSimpleName(); traceID = classN + ":" + statementID; /** logging classname */ loggingClassName = "com.microsoft.sqlserver.jdbc." + classN + ":" + statementID; stmtPoolable = false; connection = con; bIsClosed = false; // Validate result set type ... if (ResultSet.TYPE_FORWARD_ONLY != nType && ResultSet.TYPE_SCROLL_SENSITIVE != nType && ResultSet.TYPE_SCROLL_INSENSITIVE != nType && SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY != nType && SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY != nType && SQLServerResultSet.TYPE_SS_SCROLL_DYNAMIC != nType && SQLServerResultSet.TYPE_SS_SCROLL_KEYSET != nType && SQLServerResultSet.TYPE_SS_SCROLL_STATIC != nType) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_unsupportedCursor"), null, true); } // ... and concurrency if (ResultSet.CONCUR_READ_ONLY != nConcur && ResultSet.CONCUR_UPDATABLE != nConcur && SQLServerResultSet.CONCUR_SS_SCROLL_LOCKS != nConcur && SQLServerResultSet.CONCUR_SS_OPTIMISTIC_CC != nConcur && SQLServerResultSet.CONCUR_SS_OPTIMISTIC_CCVAL != nConcur) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_unsupportedConcurrency"), null, true); } if (null == stmtColEncSetting) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_unsupportedStmtColEncSetting"), null, true); } stmtColumnEncriptionSetting = stmtColEncSetting; resultSetConcurrency = nConcur; // App result set type is always whatever was used to create the statement. // This value will be returned by ResultSet.getType() calls. appResultSetType = nType; // SQL Server result set type is used everwhere other than ResultSet.getType() and // may need to be inferred based on the app result set type and the value of // the selectMethod connection property. if (ResultSet.TYPE_FORWARD_ONLY == nType) { if (ResultSet.CONCUR_READ_ONLY == nConcur) { // Check selectMethod and set to TYPE_SS_DIRECT_FORWARD_ONLY or // TYPE_SS_SERVER_CURSOR_FORWARD_ONLY accordingly. String selectMethod = con.getSelectMethod(); resultSetType = (null == selectMethod || !"cursor".equals(selectMethod)) ? SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY : // Default // forward-only, // read-only // cursor // type SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY; } else { resultSetType = SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY; } } else if (ResultSet.TYPE_SCROLL_INSENSITIVE == nType) { resultSetType = SQLServerResultSet.TYPE_SS_SCROLL_STATIC; } else if (ResultSet.TYPE_SCROLL_SENSITIVE == nType) { resultSetType = SQLServerResultSet.TYPE_SS_SCROLL_KEYSET; } else // App specified one of the SQL Server types { resultSetType = nType; } // Figure out default fetch direction nFetchDirection = (SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY == resultSetType || SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY == resultSetType) ? ResultSet.FETCH_FORWARD : ResultSet.FETCH_UNKNOWN; // Figure out fetch size: // // Too many scroll locks adversely impact concurrency on the server // and thus system performance, so default to a low value when using // scroll locks. Otherwise, use a larger fetch size. nFetchSize = (SQLServerResultSet.CONCUR_SS_SCROLL_LOCKS == resultSetConcurrency) ? 8 : 128; // Save off the default fetch size so it can be restored by setFetchSize(0). defaultFetchSize = nFetchSize; // Check compatibility between the result set type and concurrency. // Against a Yukon server, certain scrollability values are incompatible // with all but read only concurrency. Against a Shiloh server, such // combinations cause the cursor to be silently "upgraded" to one that // works. We enforce the more restrictive behavior of the two here. if (ResultSet.CONCUR_READ_ONLY != nConcur && (SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY == resultSetType || SQLServerResultSet.TYPE_SS_SCROLL_STATIC == resultSetType)) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_unsupportedCursorAndConcurrency"), null, true); } // All result set types other than firehose (SQL Server default) use server side cursors. setResponseBuffering(connection.getResponseBuffering()); setDefaultQueryTimeout(); setDefaultQueryCancelTimeout(); if (stmtlogger.isLoggable(java.util.logging.Level.FINER)) { stmtlogger.finer("Properties for " + toString() + ":" + " Result type:" + appResultSetType + " (" + resultSetType + ")" + " Concurrency:" + resultSetConcurrency + " Fetchsize:" + nFetchSize + " bIsClosed:" + bIsClosed + " useLastUpdateCount:" + connection.useLastUpdateCount()); } if (stmtlogger.isLoggable(java.util.logging.Level.FINE)) { stmtlogger.fine(toString() + " created by (" + connection.toString() + ")"); } } private void setDefaultQueryCancelTimeout() { int timeout = this.connection.getCancelQueryTimeoutSeconds(); if (timeout > 0) { this.cancelQueryTimeoutSeconds = timeout; } } // add query timeout to statement private void setDefaultQueryTimeout() { int queryTimeoutSeconds = this.connection.getQueryTimeoutSeconds(); if (queryTimeoutSeconds > 0) { this.queryTimeout = queryTimeoutSeconds; } } final java.util.logging.Logger getStatementLogger() { return stmtlogger; } /** * Closes the statement. * * Note that the public close() method performs all of the cleanup work through this internal method which cannot * throw any exceptions. This is done deliberately to ensure that ALL of the object's client-side and server-side * state is cleaned up as best as possible, even under conditions which would normally result in exceptions being * thrown. */ void closeInternal() { // Regardless what happens when cleaning up, // the statement is considered closed. assert !bIsClosed; discardLastExecutionResults(); bIsClosed = true; autoGeneratedKeys = null; sqlWarnings = null; inOutParam = null; connection.removeOpenStatement(this); } @Override public void close() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "close"); if (!bIsClosed) closeInternal(); loggerExternal.exiting(getClassNameLogging(), "close"); } @Override public void closeOnCompletion() throws SQLException { loggerExternal.entering(getClassNameLogging(), "closeOnCompletion"); checkClosed(); // enable closeOnCompletion feature isCloseOnCompletion = true; loggerExternal.exiting(getClassNameLogging(), "closeOnCompletion"); } @Override public java.sql.ResultSet executeQuery(String sql) throws SQLServerException, SQLTimeoutException { loggerExternal.entering(getClassNameLogging(), "executeQuery", sql); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE_QUERY, NO_GENERATED_KEYS)); loggerExternal.exiting(getClassNameLogging(), "executeQuery", resultSet); return resultSet; } final SQLServerResultSet executeQueryInternal(String sql) throws SQLServerException, SQLTimeoutException { checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE_QUERY_INTERNAL, NO_GENERATED_KEYS)); return resultSet; } @Override public int executeUpdate(String sql) throws SQLServerException, SQLTimeoutException { loggerExternal.entering(getClassNameLogging(), "executeUpdate", sql); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, NO_GENERATED_KEYS)); // this shouldn't happen, caller probably meant to call executeLargeUpdate if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); loggerExternal.exiting(getClassNameLogging(), "executeUpdate", updateCount); return (int) updateCount; } @Override public long executeLargeUpdate(String sql) throws SQLServerException, SQLTimeoutException { loggerExternal.entering(getClassNameLogging(), "executeLargeUpdate", sql); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, NO_GENERATED_KEYS)); loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount); return updateCount; } @Override public boolean execute(String sql) throws SQLServerException, SQLTimeoutException { loggerExternal.entering(getClassNameLogging(), "execute", sql); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); executeStatement(new StmtExecCmd(this, sql, EXECUTE, NO_GENERATED_KEYS)); loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; } /** * Statement exec command */ private final class StmtExecCmd extends TDSCommand { /** * Always update serialVersionUID when prompted. */ private static final long serialVersionUID = 4534132352812876292L; final SQLServerStatement stmt; final String sql; final int executeMethod; final int autoGeneratedKeys; StmtExecCmd(SQLServerStatement stmt, String sql, int executeMethod, int autoGeneratedKeys) { super(stmt.toString() + " executeXXX", stmt.queryTimeout, stmt.cancelQueryTimeoutSeconds); this.stmt = stmt; this.sql = sql; this.executeMethod = executeMethod; this.autoGeneratedKeys = autoGeneratedKeys; } final boolean doExecute() throws SQLServerException { stmt.doExecuteStatement(this); return false; } @Override final void processResponse(TDSReader tdsReader) throws SQLServerException { ensureExecuteResultsReader(tdsReader); processExecuteResults(); } } private String ensureSQLSyntax(String sql) throws SQLServerException { if (sql.indexOf(LEFT_CURLY_BRACKET) >= 0) { CityHash128Key cacheKey = new CityHash128Key(sql); // Check for cached SQL metadata. ParsedSQLCacheItem cacheItem = getCachedParsedSQL(cacheKey); if (null == cacheItem) cacheItem = parseAndCacheSQL(cacheKey, sql); // Retrieve from cache item. procedureName = cacheItem.procedureName; return cacheItem.processedSQL; } return sql; } void startResults() { moreResults = true; } final void setMaxRowsAndMaxFieldSize() throws SQLServerException { if (EXECUTE_QUERY == executeMethod || EXECUTE == executeMethod) { connection.setMaxRows(maxRows); connection.setMaxFieldSize(maxFieldSize); } else { assert EXECUTE_UPDATE == executeMethod || EXECUTE_BATCH == executeMethod || EXECUTE_QUERY_INTERNAL == executeMethod; // If we are executing via any of the above methods then make sure any // previous maxRows limitation on the connection is removed. connection.setMaxRows(0); } } final void doExecuteStatement(StmtExecCmd execCmd) throws SQLServerException { resetForReexecute(); // Set this command as the current command executeMethod = execCmd.executeMethod; // Apps can use JDBC call syntax to call unparameterized stored procedures // through regular Statement objects. We need to ensure that any such JDBC // call syntax is rewritten here as SQL exec syntax. String sql = ensureSQLSyntax(execCmd.sql); if (!isInternalEncryptionQuery && connection.isAEv2()) { execCmd.enclaveCEKs = connection.initEnclaveParameters(this, sql, null, null, null); } // If this request might be a query (as opposed to an update) then make // sure we set the max number of rows and max field size for any ResultSet // that may be returned. // // If the app uses Statement.execute to execute an UPDATE or DELETE statement // and has called Statement.setMaxRows to limit the number of rows from an // earlier query, then the number of rows updated/deleted will be limited as // well. // // Note: similar logic in SQLServerPreparedStatement.doExecutePreparedStatement setMaxRowsAndMaxFieldSize(); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } if (isCursorable(executeMethod) && isSelect(sql)) { if (stmtlogger.isLoggable(java.util.logging.Level.FINE)) stmtlogger.fine(toString() + " Executing server side cursor " + sql); doExecuteCursored(execCmd, sql); } else // Non-cursored execution (includes EXECUTE_QUERY_INTERNAL) { executedSqlDirectly = true; expectCursorOutParams = false; TDSWriter tdsWriter = execCmd.startRequest(TDS.PKT_QUERY); tdsWriter.sendEnclavePackage(sql, execCmd.enclaveCEKs); tdsWriter.writeString(sql); // If this is an INSERT statement and generated keys were requested // then add on the query to return them. if (RETURN_GENERATED_KEYS == execCmd.autoGeneratedKeys && (EXECUTE_UPDATE == executeMethod || EXECUTE == executeMethod) && sql.trim().toUpperCase().startsWith("INSERT")) { tdsWriter.writeString(IDENTITY_QUERY); } if (stmtlogger.isLoggable(java.util.logging.Level.FINE)) stmtlogger.fine(toString() + " Executing (not server cursor) " + sql); // Start the response ensureExecuteResultsReader(execCmd.startResponse(isResponseBufferingAdaptive)); startResults(); getNextResult(true); } // If execution produced no result set, then throw an exception if executeQuery() was used. if (null == resultSet) { if (EXECUTE_QUERY == executeMethod) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_noResultset"), null, true); } } // Otherwise, if execution produced a result set, then throw an exception // if executeUpdate() or executeBatch() was used. else { if (EXECUTE_UPDATE == executeMethod || EXECUTE_BATCH == executeMethod) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, false); } } } /** * Statement batch exec command */ private final class StmtBatchExecCmd extends TDSCommand { /** * Always update serialVersionUID when prompted. */ private static final long serialVersionUID = -4621631860790243331L; final SQLServerStatement stmt; StmtBatchExecCmd(SQLServerStatement stmt) { super(stmt.toString() + " executeBatch", stmt.queryTimeout, stmt.cancelQueryTimeoutSeconds); this.stmt = stmt; } final boolean doExecute() throws SQLServerException { stmt.doExecuteStatementBatch(this); return false; } @Override final void processResponse(TDSReader tdsReader) throws SQLServerException { ensureExecuteResultsReader(tdsReader); processExecuteResults(); } } private void doExecuteStatementBatch(StmtBatchExecCmd execCmd) throws SQLServerException { resetForReexecute(); // Make sure any previous maxRows limitation on the connection is removed. connection.setMaxRows(0); String batchStatementString = String.join(";", batchStatementBuffer); if (connection.isAEv2()) { execCmd.enclaveCEKs = connection.initEnclaveParameters(this, batchStatementString, null, null, null); } if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } // Batch execution is always non-cursored executeMethod = EXECUTE_BATCH; executedSqlDirectly = true; expectCursorOutParams = false; TDSWriter tdsWriter = execCmd.startRequest(TDS.PKT_QUERY); // Write the concatenated batch of statements, delimited by semicolons tdsWriter.sendEnclavePackage(batchStatementString, execCmd.enclaveCEKs); tdsWriter.writeString(batchStatementString); // Start the response ensureExecuteResultsReader(execCmd.startResponse(isResponseBufferingAdaptive)); startResults(); getNextResult(true); // If execution produced a result set, then throw an exception if (null != resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, false); } } /** * Resets the state to get the statement for reexecute callable statement overrides this. */ final void resetForReexecute() { ensureExecuteResultsReader(null); autoGeneratedKeys = null; updateCount = -1; sqlWarnings = null; executedSqlDirectly = false; startResults(); } /** * Determines if the SQL is a SELECT. * * @param sql * The statement SQL. * @return True if the statement is a select. */ final boolean isSelect(String sql) throws SQLServerException { checkClosed(); // Used to check just the first letter which would cause // "Set" commands to return true... String temp = sql.trim(); if (sql.length() < 6) { return false; } return "select".equalsIgnoreCase(temp.substring(0, 6)); } /** * Determine if the SQL is a INSERT. * * @param sql * The statement SQL. * @return True if the statement is an insert. */ final boolean isInsert(String sql) throws SQLServerException { checkClosed(); // Used to check just the first letter which would cause // "Set" commands to return true... String temp = sql.trim(); if (sql.length() < 6) { return false; } if ("/*".equalsIgnoreCase(temp.substring(0, 2))) { int index = temp.indexOf("*/") + 2; return isInsert(temp.substring(index)); } return "insert".equalsIgnoreCase(temp.substring(0, 6)); } /** * Replaces a JDBC parameter marker with the parameter's string value * * @param str * the parameter syntax * @param marker * the parameter marker * @param replaceStr * the param value * @return String */ static String replaceParameterWithString(String str, char marker, String replaceStr) { int index = 0; while ((index = str.indexOf("" + marker)) >= 0) { str = str.substring(0, index) + replaceStr + str.substring(index + 1, str.length()); } return str; } /** * Sets a JDBC parameter to null. (Used only when processing LOB column sources.) * * @param sql * the parameter syntax * @return the result */ static String replaceMarkerWithNull(String sql) { // replace all top(?) to top(1) as null is not valid if (TOP_SYNTAX.matcher(sql).find()) { sql = TOP_SYNTAX.matcher(sql).replaceAll("TOP(1)"); } if (!sql.contains("'")) { return replaceParameterWithString(sql, '?', "null"); } else { StringTokenizer st = new StringTokenizer(sql, "'", true); boolean beforeColon = true; final StringBuilder retSql = new StringBuilder(); while (st.hasMoreTokens()) { String str = st.nextToken(); if ("'".equals(str)) { retSql.append("'"); beforeColon = !beforeColon; continue; } if (beforeColon) { String repStr = replaceParameterWithString(str, '?', "null"); retSql.append(repStr); continue; } else { retSql.append(str); continue; } } return retSql.toString(); } } void checkClosed() throws SQLServerException { // Check the connection first so that Statement methods // throw a "Connection closed" exception if the reason // that the statement was closed is because the connection // was closed. connection.checkClosed(); if (bIsClosed) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_statementIsClosed"), null, false); } } /* ---------------- JDBC API methods ------------------ */ @Override public final int getMaxFieldSize() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getMaxFieldSize"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getMaxFieldSize", maxFieldSize); return maxFieldSize; } @Override public final void setMaxFieldSize(int max) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "setMaxFieldSize", max); checkClosed(); if (max < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); Object[] msgArgs = {max}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } maxFieldSize = max; loggerExternal.exiting(getClassNameLogging(), "setMaxFieldSize"); } @Override public final int getMaxRows() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getMaxRows"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getMaxRows", maxRows); return maxRows; } @Override public final long getLargeMaxRows() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getLargeMaxRows"); // SQL Server only supports integer limits for setting max rows. // So, getLargeMaxRows() and getMaxRows() will return the same value. loggerExternal.exiting(getClassNameLogging(), "getLargeMaxRows", (long) maxRows); return (long) getMaxRows(); } @Override public final void setMaxRows(int max) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setMaxRows", max); checkClosed(); if (max < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidRowcount")); Object[] msgArgs = {max}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } // DYNAMIC scrollable server cursors have no notion of a keyset. That is, the number of rows in // the ResultSet can change dynamically as the app scrolls around. So there is no way to support // maxRows with DYNAMIC scrollable cursors. if (SQLServerResultSet.TYPE_SS_SCROLL_DYNAMIC != resultSetType) maxRows = max; loggerExternal.exiting(getClassNameLogging(), "setMaxRows"); } @Override public final void setLargeMaxRows(long max) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setLargeMaxRows", max); // SQL server only supports integer limits for setting max rows. // If is bigger than integer limits then throw an exception, otherwise call setMaxRows(int) if (max > Integer.MAX_VALUE) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidMaxRows")); Object[] msgArgs = {max}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } setMaxRows((int) max); loggerExternal.exiting(getClassNameLogging(), "setLargeMaxRows"); } @Override public final void setEscapeProcessing(boolean enable) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setEscapeProcessing", enable); checkClosed(); escapeProcessing = enable; loggerExternal.exiting(getClassNameLogging(), "setEscapeProcessing"); } @Override public final int getQueryTimeout() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getQueryTimeout"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getQueryTimeout", queryTimeout); return queryTimeout; } @Override public final void setQueryTimeout(int seconds) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "setQueryTimeout", seconds); checkClosed(); if (seconds < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidQueryTimeOutValue")); Object[] msgArgs = {seconds}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } queryTimeout = seconds; loggerExternal.exiting(getClassNameLogging(), "setQueryTimeout"); } @Override public final int getCancelQueryTimeout() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getCancelQueryTimeout"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getCancelQueryTimeout", cancelQueryTimeoutSeconds); return cancelQueryTimeoutSeconds; } @Override public final void setCancelQueryTimeout(int seconds) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "setCancelQueryTimeout", seconds); checkClosed(); if (seconds < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidCancelQueryTimeout")); Object[] msgArgs = {seconds}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, true); } cancelQueryTimeoutSeconds = seconds; loggerExternal.exiting(getClassNameLogging(), "setCancelQueryTimeout"); } @Override public final void cancel() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "cancel"); checkClosed(); // Cancel the currently executing statement. if (null != currentCommand) currentCommand.interrupt(SQLServerException.getErrString("R_queryCanceled")); loggerExternal.exiting(getClassNameLogging(), "cancel"); } /** the SQL warnings chain */ Vector sqlWarnings; /** Flag to indicate that it is an internal query to retrieve encryption metadata. */ boolean isInternalEncryptionQuery; @Override public final SQLWarning getWarnings() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getWarnings"); checkClosed(); if (sqlWarnings == null) return null; SQLWarning warn = sqlWarnings.elementAt(0); loggerExternal.exiting(getClassNameLogging(), "getWarnings", warn); return warn; } @Override public final void clearWarnings() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "clearWarnings"); checkClosed(); sqlWarnings = null; loggerExternal.exiting(getClassNameLogging(), "clearWarnings"); } @Override public final void setCursorName(String name) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "setCursorName", name); checkClosed(); cursorName = name; loggerExternal.exiting(getClassNameLogging(), "setCursorName"); } final String getCursorName() { return cursorName; } @Override public final java.sql.ResultSet getResultSet() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getResultSet"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getResultSet", resultSet); return resultSet; } @Override public final int getUpdateCount() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getUpdateCount"); checkClosed(); // this shouldn't happen, caller probably meant to call getLargeUpdateCount if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); loggerExternal.exiting(getClassNameLogging(), "getUpdateCount", updateCount); return (int) updateCount; } @Override public final long getLargeUpdateCount() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getUpdateCount"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getUpdateCount", updateCount); return updateCount; } final void ensureExecuteResultsReader(TDSReader tdsReader) { this.tdsReader = tdsReader; } final void processExecuteResults() throws SQLServerException { if (wasExecuted()) { processBatch(); checkClosed(); // processBatch could have resulted in a closed connection if isCloseOnCompletion is set TDSParser.parse(resultsReader(), "batch completion"); ensureExecuteResultsReader(null); } } void processBatch() throws SQLServerException { processResults(); } final void processResults() throws SQLServerException { SQLServerException interruptException = null; while (moreResults) { // Get the next result try { getNextResult(true); } catch (SQLServerException e) { // If an exception is thrown while processing the results // then decide what to do with it: if (moreResults) { // Silently discard database errors and continue processing the remaining results if (SQLServerException.DRIVER_ERROR_FROM_DATABASE == e.getDriverErrorCode()) { if (stmtlogger.isLoggable(java.util.logging.Level.FINEST)) { stmtlogger.finest( this + " ignoring database error: " + e.getErrorCode() + " " + e.getMessage()); } continue; } // If statement execution was canceled then continue processing the // remaining results before throwing the "statement canceled" exception. if (e.getSQLState() != null && e.getSQLState().equals(SQLState.STATEMENT_CANCELED.getSQLStateCode())) { interruptException = e; continue; } } // Propagate up more serious exceptions to the caller moreResults = false; throw e; } } clearLastResult(); if (null != interruptException) throw interruptException; } /** * Returns more results in the TDS stream. * * @return true if the next result is a ResultSet object; false if it is an integer (indicating that it is an update * count or there are no more results). */ @Override public final boolean getMoreResults() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getMoreResults"); checkClosed(); // Get the next result, whatever it is (ResultSet or update count). // Don't just return the value from the getNextResult() call, however. // The getMoreResults method has a subtle spec for its return value (see above). getNextResult(true); loggerExternal.exiting(getClassNameLogging(), "getMoreResults", null != resultSet); return null != resultSet; } /** * Clears the most recent result (ResultSet or update count) from this statement. * * This method is used to clean up before moving to the next result and after processing the last result. * * Note that errors closing the ResultSet (if the last result is a ResultSet) are caught, logged, and ignored. The * reason for this is that this method is only for cleanup for when the app fails to do the cleanup itself (for * example by leaving a ResultSet open when closing the statement). If the app wants to be able to handle errors * from closing the current result set, then it should close that ResultSet itself before closing the statement, * moving to the next result, or whatever. This is the recommended practice with JDBC. */ final void clearLastResult() { // Clear any update count result updateCount = -1; // Clear any ResultSet result if (null != resultSet) { // Try closing the ResultSet. If closing fails, log the error and ignore it. // Then just drop our reference to the ResultSet object -- we're done with // it anyhow. try { resultSet.close(); } catch (SQLServerException e) { stmtlogger.finest(this + " clearing last result; ignored error closing ResultSet: " + e.getErrorCode() + " " + e.getMessage()); } finally { resultSet = null; } } } /** * Returns the next result in the TDS response token stream, which may be a result set, update count or exception. * * @param clearFlag * Boolean Flag if set to true, clears the last stored results in ResultSet. If set to false, does not clear * ResultSet and continues processing TDS Stream for more results. * * @return true if another result (ResultSet or update count) was available; false if there were no more results. */ final boolean getNextResult(boolean clearFlag) throws SQLServerException { /** * TDS response token stream handler used to locate the next result in the TDS response token stream. */ final class NextResult extends TDSTokenHandler { private StreamDone stmtDoneToken = null; final boolean isUpdateCount() { return null != stmtDoneToken; } final long getUpdateCount() { return stmtDoneToken.getUpdateCount(); } private boolean isResultSet = false; final boolean isResultSet() { return isResultSet; } private StreamRetStatus procedureRetStatToken = null; NextResult() { super("getNextResult"); } @Override boolean onColMetaData(TDSReader tdsReader) throws SQLServerException { /* * If we have an update count from a previous command that we haven't acknowledged because we didn't * know at the time whether it was the undesired result from a trigger, and if we did not encounter an * ERROR token before hitting this COLMETADATA token, with any intervening DONE token (does not indicate * an error result), then go ahead. */ if (null == stmtDoneToken && null == getDatabaseError()) { // If both conditions are true, column metadata indicates the start of a ResultSet isResultSet = true; } return false; } @Override boolean onDone(TDSReader tdsReader) throws SQLServerException { // Consume the done token and decide what to do with it... // Handling DONE/DONEPROC/DONEINPROC tokens is a little tricky... StreamDone doneToken = new StreamDone(); doneToken.setFromTDS(tdsReader); if (doneToken.isFinal()) { connection.getSessionRecovery().decrementUnprocessedResponseCount(); } // If the done token has the attention ack bit set, then record // it as the attention ack DONE token. We may or may not throw // an statement canceled/timed out exception later based on // whether we've seen this ack. if (doneToken.isAttnAck()) return false; // The meaning of other done tokens depends on several things ... // // A done token from a DML or DDL command normally contains an update // count that is usually returned as the next result. But processing // doesn't stop until we reach the end of the result, which may include // other tokens. And there are exceptions to that rule ... if (doneToken.cmdIsDMLOrDDL()) { // If update counts are suppressed (i.e. SET NOCOUNT ON was used) then // the DML/DDL update count is not valid, and this result should be skipped // unless it's for a batch where it's ok to report a "done without count" // status (Statement.SUCCESS_NO_INFO) if (-1 == doneToken.getUpdateCount() && EXECUTE_BATCH != executeMethod) return true; if (-1 != doneToken.getUpdateCount() && EXECUTE_QUERY == executeMethod) return true; // Otherwise, the update count is valid. Now determine whether we should // return it as a result... stmtDoneToken = doneToken; // Always return update counts from directly executed SQL statements. // This encompases both batch execution in normal SQLServerStatement objects // and compound statement execution, which are indistinguishable from one // another as they are both just concatenated statements. if (TDS.TDS_DONEINPROC != doneToken.getTokenType()) return false; if (EXECUTE_BATCH != executeMethod) { // Always return update counts from statements executed in stored procedure calls. if (null != procedureName) return false; // Always return all update counts from statements executed through Statement.execute() if (EXECUTE == executeMethod) return false; // Statement.executeUpdate() may or may not return this update count depending on the // setting of the lastUpdateCount connection property: // // If lastUpdateCount=true (the default), then skip this update count and continue. // Otherwise (lastUpdateCount=false), return this update count. if (!connection.useLastUpdateCount()) return false; // An update count from a statement that appears to have been executed in a stored // procedure call, when no stored procedure was called, is assumed to be the result of // a trigger firing. Skip it for now until/unless it is shown to be the last update // count of a statement's execution. } } // A done token without an update count usually just indicates the execution // of a SQL statement that isn't an INSERT, UPDATE, DELETE or DDL, and it should // be ignored. However, some of these done tokens have special meaning that we // need to consider... else { // The final done token in the response always marks the end of the result, // even if there is no update count. if (doneToken.isFinal()) { moreResults = false; return false; } if (EXECUTE_BATCH == executeMethod) { // The done token that marks the end of a batch always marks the end // of the result, even if there is no update count. if (TDS.TDS_DONEINPROC != doneToken.getTokenType() || doneToken.wasRPCInBatch()) { moreResults = false; return false; } } } // If the current command (whatever it was) produced an error then stop parsing and propagate it up. // In this case, the command is likely to be a RAISERROR, but it could be anything. if (doneToken.isError()) return false; // In all other cases, keep parsing return true; } @Override boolean onRetStatus(TDSReader tdsReader) throws SQLServerException { // If this return status token marks the start of statement execution OUT parameters, // then consume those OUT parameters, setting server cursor ID, row count, and/or // the prepared statement handle from them. if (consumeExecOutParam(tdsReader)) { moreResults = false; } // If this RETSTATUS token is not the one that begins the statement execution OUT // parameters, then it must be one from a stored procedure that was called by // the query itself. In that case, it should be ignored as it does not contribute // to any result. It may mark the start of application OUT parameters, however, // so remember that we've seen it so that we don't ignore the RETVALUE tokens that // may follow. else { procedureRetStatToken = new StreamRetStatus(); procedureRetStatToken.setFromTDS(tdsReader); } return true; } @Override boolean onRetValue(TDSReader tdsReader) throws SQLServerException { // We are only interested in return values that are statement OUT parameters, // in which case we need to stop parsing and let CallableStatement take over. // A RETVALUE token appearing in the execution results, but before any RETSTATUS // token, is a TEXTPTR return value that should be ignored. if (moreResults && null == procedureRetStatToken) { Parameter p = new Parameter( Util.shouldHonorAEForParameters(stmtColumnEncriptionSetting, connection)); p.skipRetValStatus(tdsReader); p.skipValue(tdsReader, true); return true; } return false; } @Override boolean onInfo(TDSReader tdsReader) throws SQLServerException { StreamInfo infoToken = new StreamInfo(); infoToken.setFromTDS(tdsReader); // Under some circumstances the server cannot produce the cursored result set // that we requested, but produces a client-side (default) result set instead. // When this happens, there are no cursor ID or rowcount OUT parameters -- the // result set rows are returned directly as if this were a client-cursored // result set. // // ErrorNumber: 16954 // ErrorSeverity: EX_USER // ErrorFormat: Executing SQL directly; no cursor. // ErrorCause: Server cursor is not supported on the specified SQL, falling back to default result set // ErrorCorrectiveAction: None required // if (16954 == infoToken.msg.getErrorNumber()) executedSqlDirectly = true; SQLWarning warning = new SQLWarning( infoToken.msg.getErrorMessage(), SQLServerException.generateStateCode(connection, infoToken.msg.getErrorNumber(), infoToken.msg.getErrorState()), infoToken.msg.getErrorNumber()); if (sqlWarnings == null) { sqlWarnings = new Vector<>(); } else { int n = sqlWarnings.size(); SQLWarning w = sqlWarnings.elementAt(n - 1); w.setNextWarning(warning); } sqlWarnings.add(warning); return true; } } // If the statement has no results yet, then return if (!wasExecuted()) { moreResults = false; return false; } // Clear out previous results only when clearFlag = true if (clearFlag) { clearLastResult(); } // If there are no more results, then we're done. All we had to do was to close out the previous results. if (!moreResults) { return false; } // Figure out the next result. NextResult nextResult = new NextResult(); // Signal to not read all token other than TDS_MSG if reading only warnings TDSParser.parse(resultsReader(), nextResult, !clearFlag); // Check for errors first. if (null != nextResult.getDatabaseError()) { SQLServerException.makeFromDatabaseError(connection, null, nextResult.getDatabaseError().getErrorMessage(), nextResult.getDatabaseError(), false); } // If we didn't clear current ResultSet, we wanted to read only warnings. Return back from here. if (!clearFlag) return false; // Not an error. Is it a result set? else if (nextResult.isResultSet()) { resultSet = new SQLServerResultSet(this); return true; } // Nope. Not an error or a result set. Maybe a result from a T-SQL statement? // That is, one of the following: // - a positive count of the number of rows affected (from INSERT, UPDATE, or DELETE), // - a zero indicating no rows affected, or the statement was DDL, or // - a -1 indicating the statement succeeded, but there is no update count // information available (translates to Statement.SUCCESS_NO_INFO in batch // update count arrays). else if (nextResult.isUpdateCount()) { updateCount = nextResult.getUpdateCount(); return true; } // None of the above. Last chance here... Going into the parser above, we know // moreResults was initially true. If we come out with moreResults false, then // we hit a DONE token (either DONE (FINAL) or DONE (RPC in batch)) that indicates // that the batch succeeded overall, but that there is no information on individual // statements' update counts. This is similar to the last case above, except that // there is no update count. That is: we have a successful result (return true), // but we have no other information about it (updateCount = -1). updateCount = -1; if (!moreResults) { return true; } // Only way to get here (moreResults is still true, but no apparent results of any kind) // is if the TDSParser didn't actually parse _anything_. That is, we are at EOF in the // response. In that case, there truly are no more results. We're done. moreResults = false; return false; } /** * Consumes the OUT parameter for the statement object itself. * * Normal Statement objects consume the server cursor OUT params when present. PreparedStatement and * CallableStatement objects override this method to consume the prepared statement handle as well. */ boolean consumeExecOutParam(TDSReader tdsReader) throws SQLServerException { if (expectCursorOutParams) { TDSParser.parse(tdsReader, new StmtExecOutParamHandler(this)); return true; } return false; } // --------------------------JDBC 2.0----------------------------- @Override public final void setFetchDirection(int nDir) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setFetchDirection", nDir); checkClosed(); if ((ResultSet.FETCH_FORWARD != nDir && ResultSet.FETCH_REVERSE != nDir && ResultSet.FETCH_UNKNOWN != nDir) || (ResultSet.FETCH_FORWARD != nDir && (SQLServerResultSet.TYPE_SS_DIRECT_FORWARD_ONLY == resultSetType || SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY == resultSetType))) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidFetchDirection")); Object[] msgArgs = {nDir}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } nFetchDirection = nDir; loggerExternal.exiting(getClassNameLogging(), "setFetchDirection"); } @Override public final int getFetchDirection() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getFetchDirection"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getFetchDirection", nFetchDirection); return nFetchDirection; } @Override public final void setFetchSize(int rows) throws SQLServerException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "setFetchSize", rows); checkClosed(); if (rows < 0) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidFetchSize"), null, false); nFetchSize = (0 == rows) ? defaultFetchSize : rows; loggerExternal.exiting(getClassNameLogging(), "setFetchSize"); } @Override public final int getFetchSize() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getFetchSize"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getFetchSize", nFetchSize); return nFetchSize; } @Override public final int getResultSetConcurrency() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getResultSetConcurrency"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getResultSetConcurrency", resultSetConcurrency); return resultSetConcurrency; } @Override public final int getResultSetType() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getResultSetType"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "getResultSetType", appResultSetType); return appResultSetType; } @Override public void addBatch(String sql) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "addBatch", sql); checkClosed(); // Apps can use JDBC call syntax to call unparameterized stored procedures // through regular Statement objects. We need to ensure that any such JDBC // call syntax is rewritten here as SQL exec syntax. sql = ensureSQLSyntax(sql); batchStatementBuffer.add(sql); loggerExternal.exiting(getClassNameLogging(), "addBatch"); } @Override public void clearBatch() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "clearBatch"); checkClosed(); batchStatementBuffer.clear(); loggerExternal.exiting(getClassNameLogging(), "clearBatch"); } /** * Sends a batch of statements to the database. */ @Override public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQLTimeoutException { loggerExternal.entering(getClassNameLogging(), "executeBatch"); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); discardLastExecutionResults(); try { // Initialize all the update counts to EXECUTE_FAILED. This is so that if the server // returns fewer update counts than we expected (for example, due to closing the connection // on a severe error), we will return errors for the unexecuted batches. final int batchSize = batchStatementBuffer.size(); int[] updateCounts = new int[batchSize]; for (int batchNum = 0; batchNum < batchSize; batchNum++) updateCounts[batchNum] = Statement.EXECUTE_FAILED; // Last exception thrown. If database errors are returned, then executeBatch throws a // BatchUpdateException with this exception and the update counts, including errors. SQLServerException lastError = null; for (int batchNum = 0; batchNum < batchSize; batchNum++) { // NOTE: // When making changes to anything below, consider whether similar changes need // to be made to PreparedStatement batch execution. try { if (0 == batchNum) { // First time through, execute the entire set of batches and return the first result executeStatement(new StmtBatchExecCmd(this)); } else { // Subsequent times through, just get the result from the next batch. // If there are not enough results (update counts) to satisfy the number of batches, // then bail, leaving EXECUTE_FAILED in the remaining slots of the update count array. startResults(); if (!getNextResult(true)) break; } if (null != resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, true); } else { updateCounts[batchNum] = (-1 != (int) updateCount) ? (int) updateCount : Statement.SUCCESS_NO_INFO; } } catch (SQLServerException e) { // If the failure was severe enough to close the connection or roll back a // manual transaction, then propagate the error up as a SQLServerException // now, rather than continue with the batch. if (connection.isSessionUnAvailable() || connection.rolledBackTransaction()) throw e; // Otherwise, the connection is OK and the transaction is still intact, // so just record the failure for the particular batch item. lastError = e; } } // If we had any errors then throw a BatchUpdateException with the partial results. if (null != lastError) { throw new BatchUpdateException(lastError.getMessage(), lastError.getSQLState(), lastError.getErrorCode(), updateCounts); } loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts); return updateCounts; } finally { // Regardless what happens, always clear out the batch after execution. // Note: Don't use the clearBatch API as it checks that the statement is // not closed, which it might be in the event of a severe error. batchStatementBuffer.clear(); } } // executeBatch @Override public long[] executeLargeBatch() throws SQLServerException, BatchUpdateException, SQLTimeoutException { loggerExternal.entering(getClassNameLogging(), "executeLargeBatch"); if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } checkClosed(); discardLastExecutionResults(); try { // Initialize all the update counts to EXECUTE_FAILED. This is so that if the server // returns fewer update counts than we expected (for example, due to closing the connection // on a severe error), we will return errors for the unexecuted batches. final int batchSize = batchStatementBuffer.size(); long[] updateCounts = new long[batchSize]; for (int batchNum = 0; batchNum < batchSize; batchNum++) updateCounts[batchNum] = Statement.EXECUTE_FAILED; // Last exception thrown. If database errors are returned, then executeBatch throws a // BatchUpdateException with this exception and the update counts, including errors. SQLServerException lastError = null; for (int batchNum = 0; batchNum < batchSize; batchNum++) { // NOTE: // When making changes to anything below, consider whether similar changes need // to be made to PreparedStatement batch execution. try { if (0 == batchNum) { // First time through, execute the entire set of batches and return the first result executeStatement(new StmtBatchExecCmd(this)); } else { // Subsequent times through, just get the result from the next batch. // If there are not enough results (update counts) to satisfy the number of batches, // then bail, leaving EXECUTE_FAILED in the remaining slots of the update count array. startResults(); if (!getNextResult(true)) break; } if (null != resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_resultsetGeneratedForUpdate"), null, true); } else { updateCounts[batchNum] = (-1 != updateCount) ? updateCount : Statement.SUCCESS_NO_INFO; } } catch (SQLServerException e) { // If the failure was severe enough to close the connection or roll back a // manual transaction, then propagate the error up as a SQLServerException // now, rather than continue with the batch. if (connection.isSessionUnAvailable() || connection.rolledBackTransaction()) throw e; // Otherwise, the connection is OK and the transaction is still intact, // so just record the failure for the particular batch item. lastError = e; } } // If we had any errors then throw a BatchUpdateException with the partial results. if (null != lastError) { DriverJDBCVersion.throwBatchUpdateException(lastError, updateCounts); } loggerExternal.exiting(getClassNameLogging(), "executeLargeBatch", updateCounts); return updateCounts; } finally { // Regardless what happens, always clear out the batch after execution. // Note: Don't use the clearBatch API as it checks that the statement is // not closed, which it might be in the event of a severe error. batchStatementBuffer.clear(); } } // executeLargeBatch /** * Returns the statement's connection. * * @throws SQLServerException * when an error occurs * @return the connection */ @Override public final java.sql.Connection getConnection() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getConnection"); if (bIsClosed) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_statementIsClosed"), null, false); } java.sql.Connection con = connection.getConnection(); loggerExternal.exiting(getClassNameLogging(), "getConnection", con); return con; } /* ----------------- Server side cursor support -------------------------- */ final int getResultSetScrollOpt() { int scrollOpt = (null == inOutParam) ? 0 : TDS.SCROLLOPT_PARAMETERIZED_STMT; switch (resultSetType) { case SQLServerResultSet.TYPE_SS_SERVER_CURSOR_FORWARD_ONLY: return scrollOpt | ((ResultSet.CONCUR_READ_ONLY == resultSetConcurrency) ? TDS.SCROLLOPT_FAST_FORWARD : TDS.SCROLLOPT_FORWARD_ONLY); case SQLServerResultSet.TYPE_SS_SCROLL_DYNAMIC: return scrollOpt | TDS.SCROLLOPT_DYNAMIC; case SQLServerResultSet.TYPE_SS_SCROLL_KEYSET: return scrollOpt | TDS.SCROLLOPT_KEYSET; case SQLServerResultSet.TYPE_SS_SCROLL_STATIC: return scrollOpt | TDS.SCROLLOPT_STATIC; // Other (invalid) values were caught by the constructor. default: return 0; } } final int getResultSetCCOpt() { switch (resultSetConcurrency) { case ResultSet.CONCUR_READ_ONLY: return TDS.CCOPT_READ_ONLY | TDS.CCOPT_ALLOW_DIRECT; case SQLServerResultSet.CONCUR_SS_OPTIMISTIC_CC: // aka case ResultSet.CONCUR_UPDATABLE: return TDS.CCOPT_OPTIMISTIC_CC | TDS.CCOPT_UPDT_IN_PLACE | TDS.CCOPT_ALLOW_DIRECT; case SQLServerResultSet.CONCUR_SS_SCROLL_LOCKS: return TDS.CCOPT_SCROLL_LOCKS | TDS.CCOPT_UPDT_IN_PLACE | TDS.CCOPT_ALLOW_DIRECT; case SQLServerResultSet.CONCUR_SS_OPTIMISTIC_CCVAL: return TDS.CCOPT_OPTIMISTIC_CCVAL | TDS.CCOPT_UPDT_IN_PLACE | TDS.CCOPT_ALLOW_DIRECT; // Other (invalid) values were caught by the constructor. default: return 0; } } private void doExecuteCursored(StmtExecCmd execCmd, String sql) throws SQLServerException { if (stmtlogger.isLoggable(java.util.logging.Level.FINER)) { stmtlogger.finer(toString() + " Execute for cursor open" + " SQL:" + sql + " Scrollability:" + getResultSetScrollOpt() + " Concurrency:" + getResultSetCCOpt()); } executedSqlDirectly = false; expectCursorOutParams = true; TDSWriter tdsWriter = execCmd.startRequest(TDS.PKT_RPC); tdsWriter.writeShort((short) 0xFFFF); // procedure name length -> use ProcIDs tdsWriter.writeShort(TDS.PROCID_SP_CURSOROPEN); tdsWriter.writeByte((byte) 0); // RPC procedure option 1 tdsWriter.writeByte((byte) 0); // RPC procedure option 2 tdsWriter.sendEnclavePackage(sql, execCmd.enclaveCEKs); // OUT tdsWriter.writeRPCInt(null, 0, true); // IN tdsWriter.writeRPCStringUnicode(sql); // IN tdsWriter.writeRPCInt(null, getResultSetScrollOpt(), false); // IN tdsWriter.writeRPCInt(null, getResultSetCCOpt(), false); // OUT tdsWriter.writeRPCInt(null, 0, true); ensureExecuteResultsReader(execCmd.startResponse(isResponseBufferingAdaptive)); startResults(); getNextResult(true); } /* JDBC 3.0 */ @Override public final int getResultSetHoldability() throws SQLException { loggerExternal.entering(getClassNameLogging(), "getResultSetHoldability"); checkClosed(); int holdability = connection.getHoldability(); // For SQL Server must be the same as the connection loggerExternal.exiting(getClassNameLogging(), "getResultSetHoldability", holdability); return holdability; } @Override public final boolean execute(java.lang.String sql, int autoGeneratedKeys) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) { loggerExternal.entering(getClassNameLogging(), "execute", new Object[] {sql, autoGeneratedKeys}); if (Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } } checkClosed(); if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidAutoGeneratedKeys")); Object[] msgArgs = {autoGeneratedKeys}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } executeStatement(new StmtExecCmd(this, sql, EXECUTE, autoGeneratedKeys)); loggerExternal.exiting(getClassNameLogging(), "execute", null != resultSet); return null != resultSet; } @Override public final boolean execute(java.lang.String sql, int[] columnIndexes) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "execute", new Object[] {sql, columnIndexes}); checkClosed(); if (columnIndexes == null || columnIndexes.length != 1) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } boolean fSuccess = execute(sql, Statement.RETURN_GENERATED_KEYS); loggerExternal.exiting(getClassNameLogging(), "execute", fSuccess); return fSuccess; } @Override public final boolean execute(java.lang.String sql, java.lang.String[] columnNames) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "execute", new Object[] {sql, columnNames}); checkClosed(); if (columnNames == null || columnNames.length != 1) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } boolean fSuccess = execute(sql, Statement.RETURN_GENERATED_KEYS); loggerExternal.exiting(getClassNameLogging(), "execute", fSuccess); return fSuccess; } @Override public final int executeUpdate(String sql, int autoGeneratedKeys) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) { loggerExternal.entering(getClassNameLogging(), "executeUpdate", new Object[] {sql, autoGeneratedKeys}); if (Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } } checkClosed(); if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidAutoGeneratedKeys")); Object[] msgArgs = {autoGeneratedKeys}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, autoGeneratedKeys)); // this shouldn't happen, caller probably meant to call executeLargeUpdate if (updateCount < Integer.MIN_VALUE || updateCount > Integer.MAX_VALUE) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_updateCountOutofRange"), null, true); loggerExternal.exiting(getClassNameLogging(), "executeUpdate", updateCount); return (int) updateCount; } @Override public final long executeLargeUpdate(String sql, int autoGeneratedKeys) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) { loggerExternal.entering(getClassNameLogging(), "executeLargeUpdate", new Object[] {sql, autoGeneratedKeys}); if (Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } } checkClosed(); if (autoGeneratedKeys != Statement.RETURN_GENERATED_KEYS && autoGeneratedKeys != Statement.NO_GENERATED_KEYS) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidAutoGeneratedKeys")); Object[] msgArgs = {autoGeneratedKeys}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } executeStatement(new StmtExecCmd(this, sql, EXECUTE_UPDATE, autoGeneratedKeys)); loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", updateCount); return updateCount; } @Override public final int executeUpdate(java.lang.String sql, int[] columnIndexes) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "executeUpdate", new Object[] {sql, columnIndexes}); checkClosed(); if (columnIndexes == null || columnIndexes.length != 1) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } int count = executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); loggerExternal.exiting(getClassNameLogging(), "executeUpdate", count); return count; } @Override public final long executeLargeUpdate(java.lang.String sql, int[] columnIndexes) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "executeLargeUpdate", new Object[] {sql, columnIndexes}); checkClosed(); if (columnIndexes == null || columnIndexes.length != 1) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } long count = executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS); loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", count); return count; } @Override public final int executeUpdate(java.lang.String sql, String[] columnNames) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "executeUpdate", new Object[] {sql, columnNames}); checkClosed(); if (columnNames == null || columnNames.length != 1) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } int count = executeUpdate(sql, Statement.RETURN_GENERATED_KEYS); loggerExternal.exiting(getClassNameLogging(), "executeUpdate", count); return count; } @Override public final long executeLargeUpdate(java.lang.String sql, String[] columnNames) throws SQLServerException, SQLTimeoutException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "executeLargeUpdate", new Object[] {sql, columnNames}); checkClosed(); if (columnNames == null || columnNames.length != 1) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_invalidColumnArrayLength"), null, false); } long count = executeLargeUpdate(sql, Statement.RETURN_GENERATED_KEYS); loggerExternal.exiting(getClassNameLogging(), "executeLargeUpdate", count); return count; } @Override public final ResultSet getGeneratedKeys() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getGeneratedKeys"); checkClosed(); if (null == autoGeneratedKeys) { long orgUpd = updateCount; // Generated keys are returned in a ResultSet result right after the update count. // Try to get that ResultSet. If there are no more results after the update count, // or if the next result isn't a ResultSet, then something is wrong. if (!getNextResult(true) || null == resultSet) { SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_statementMustBeExecuted"), null, false); } autoGeneratedKeys = resultSet; updateCount = orgUpd; } loggerExternal.exiting(getClassNameLogging(), "getGeneratedKeys", autoGeneratedKeys); return autoGeneratedKeys; } @Override public final boolean getMoreResults(int mode) throws SQLException { if (loggerExternal.isLoggable(java.util.logging.Level.FINER)) loggerExternal.entering(getClassNameLogging(), "getMoreResults", mode); checkClosed(); if (KEEP_CURRENT_RESULT == mode) { SQLServerException.throwNotSupportedException(connection, this); } if (CLOSE_CURRENT_RESULT != mode && CLOSE_ALL_RESULTS != mode) SQLServerException.makeFromDriverError(connection, this, SQLServerException.getErrString("R_modeSuppliedNotValid"), null, true); ResultSet rsPrevious = resultSet; boolean fResults = getMoreResults(); if (rsPrevious != null) { try { rsPrevious.close(); } catch (SQLException e) { throw new SQLServerException(e.getMessage(), null, 0, e); } } loggerExternal.exiting(getClassNameLogging(), "getMoreResults", fResults); return fResults; } @Override public boolean isClosed() throws SQLException { loggerExternal.entering(getClassNameLogging(), "isClosed"); boolean result = bIsClosed || connection.isSessionUnAvailable(); loggerExternal.exiting(getClassNameLogging(), "isClosed", result); return result; } @Override public boolean isCloseOnCompletion() throws SQLException { loggerExternal.entering(getClassNameLogging(), "isCloseOnCompletion"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "isCloseOnCompletion", isCloseOnCompletion); return isCloseOnCompletion; } @Override public boolean isPoolable() throws SQLException { loggerExternal.entering(getClassNameLogging(), "isPoolable"); checkClosed(); loggerExternal.exiting(getClassNameLogging(), "isPoolable", stmtPoolable); return stmtPoolable; } @Override public void setPoolable(boolean poolable) throws SQLException { loggerExternal.entering(getClassNameLogging(), "setPoolable", poolable); checkClosed(); stmtPoolable = poolable; loggerExternal.exiting(getClassNameLogging(), "setPoolable"); } @Override public boolean isWrapperFor(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "isWrapperFor"); boolean f = iface.isInstance(this); loggerExternal.exiting(getClassNameLogging(), "isWrapperFor", f); return f; } @Override public T unwrap(Class iface) throws SQLException { loggerExternal.entering(getClassNameLogging(), "unwrap"); T t; try { t = iface.cast(this); } catch (ClassCastException e) { throw new SQLServerException(e.getMessage(), e); } loggerExternal.exiting(getClassNameLogging(), "unwrap", t); return t; } // responseBuffering controls the driver's buffering of responses from SQL Server. // Possible values are: // // "full" - Fully buffer the response at execution time. // Advantages: // 100% back compat with v1.1 driver // Maximizes concurrency on the server // Disadvantages: // Consumes more client-side memory // Client scalability limits with large responses // More execute latency // // "adaptive" - Data Pipe adaptive buffering // Advantages: // Buffers only when necessary, only as much as necessary // Enables handling very large responses, values // Disadvantages // Reduced concurrency on the server @Override public final void setResponseBuffering(String value) throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "setResponseBuffering", value); checkClosed(); if ("full".equalsIgnoreCase(value)) { isResponseBufferingAdaptive = false; wasResponseBufferingSet = true; } else if ("adaptive".equalsIgnoreCase(value)) { isResponseBufferingAdaptive = true; wasResponseBufferingSet = true; } else { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidresponseBuffering")); Object[] msgArgs = {value}; SQLServerException.makeFromDriverError(connection, this, form.format(msgArgs), null, false); } loggerExternal.exiting(getClassNameLogging(), "setResponseBuffering"); } @Override public final String getResponseBuffering() throws SQLServerException { loggerExternal.entering(getClassNameLogging(), "getResponseBuffering"); checkClosed(); String responseBuff; if (wasResponseBufferingSet) { if (isResponseBufferingAdaptive) responseBuff = "adaptive"; else responseBuff = "full"; } else { responseBuff = connection.getResponseBuffering(); } loggerExternal.exiting(getClassNameLogging(), "getResponseBuffering", responseBuff); return responseBuff; } /** This is a per-statement store provider. */ transient Map statementColumnEncryptionKeyStoreProviders = new HashMap<>(); /** reentrant lock */ private final transient Lock lock = new ReentrantLock(); /** * Registers statement-level key store providers, replacing all existing providers. * * @param clientKeyStoreProviders * a map containing the store providers information. * @throws SQLServerException * when an error occurs */ public void registerColumnEncryptionKeyStoreProvidersOnStatement( Map clientKeyStoreProviders) throws SQLServerException { loggerExternal.entering(loggingClassName, "registerColumnEncryptionKeyStoreProvidersOnStatement", "Registering Column Encryption Key Store Providers on Statement"); lock.lock(); try { checkClosed(); if (null == clientKeyStoreProviders) { throw new SQLServerException(null, SQLServerException.getErrString("R_CustomKeyStoreProviderMapNull"), null, 0, false); } statementColumnEncryptionKeyStoreProviders.clear(); for (Map.Entry entry : clientKeyStoreProviders .entrySet()) { String providerName = entry.getKey(); if (null == providerName || 0 == providerName.trim().length()) { throw new SQLServerException(null, SQLServerException.getErrString("R_EmptyCustomKeyStoreProviderName"), null, 0, false); } // MSSQL_CERTIFICATE_STORE not allowed on statement level if ((providerName.equalsIgnoreCase(WINDOWS_KEY_STORE_NAME))) { MessageFormat form = new MessageFormat( SQLServerException.getErrString("R_InvalidCustomKeyStoreProviderName")); Object[] msgArgs = {providerName, WINDOWS_KEY_STORE_NAME}; throw new SQLServerException(null, form.format(msgArgs), null, 0, false); } if (null == entry.getValue()) { throw new SQLServerException(null, String .format(SQLServerException.getErrString("R_CustomKeyStoreProviderValueNull"), providerName), null, 0, false); } statementColumnEncryptionKeyStoreProviders.put(entry.getKey(), entry.getValue()); } } finally { lock.unlock(); } loggerExternal.exiting(loggingClassName, "registerColumnEncryptionKeyStoreProvidersOnStatement", "Number of statement-level Key store providers that are registered: " + statementColumnEncryptionKeyStoreProviders.size()); } String getAllStatementColumnEncryptionKeyStoreProviders() { lock.lock(); try { String keyStores = ""; if (0 != statementColumnEncryptionKeyStoreProviders.size()) { keyStores = statementColumnEncryptionKeyStoreProviders.keySet().toString(); } return keyStores; } finally { lock.unlock(); } } boolean hasColumnEncryptionKeyStoreProvidersRegistered() { lock.lock(); try { return null != statementColumnEncryptionKeyStoreProviders && statementColumnEncryptionKeyStoreProviders.size() > 0; } finally { lock.unlock(); } } SQLServerColumnEncryptionKeyStoreProvider getColumnEncryptionKeyStoreProvider( String providerName) throws SQLServerException { lock.lock(); try { // Check for a statement-level provider first if (null != statementColumnEncryptionKeyStoreProviders && statementColumnEncryptionKeyStoreProviders.size() > 0) { // If any statement-level providers are registered, we don't fall back to connection-level providers if (statementColumnEncryptionKeyStoreProviders.containsKey(providerName)) { return statementColumnEncryptionKeyStoreProviders.get(providerName); } else { MessageFormat form = new MessageFormat( SQLServerException.getErrString("R_UnrecognizedStatementKeyStoreProviderName")); Object[] msgArgs = {providerName, getAllStatementColumnEncryptionKeyStoreProviders()}; throw new SQLServerException(form.format(msgArgs), null); } } return null; } finally { lock.unlock(); } } } /** * Provides a helper class that does some basic parsing work for SQL statements that are stored procedure calls. * * - Determines whether the SQL uses JDBC call syntax ("{[? =] call procedure_name...}") or T-SQL EXECUTE syntax ("EXEC * [@p0 =] procedure_name..."). If JDBC call syntax is present, it gets rewritten as T-SQL EXECUTE syntax. * * - Determines whether the caller expects a return value from the stored procedure (see the optional return value * syntax in [] above). * * - Extracts the stored procedure name from the call. * * SQL statements that are not stored procedure calls are passed through unchanged. */ final class JDBCSyntaxTranslator { private String procedureName = null; String getProcedureName() { return procedureName; } private boolean hasReturnValueSyntax = false; boolean hasReturnValueSyntax() { return hasReturnValueSyntax; } /* * SQL Identifier regex Loosely follows the spec'd SQL identifier syntax: - anything between escape characters * (square brackets or double quotes), including escaped escape characters, OR - any contiguous string of * non-whitespace characters. - including multipart identifiers */ private final static String SQL_IDENTIFIER_PART = "(?:(?:\\[(?:[^\\]]|(?:\\]\\]))+?\\])|(?:\"(?:[^\"]|(?:\"\"))+?\")|(?:\\S+?))"; private final static String SQL_IDENTIFIER_WITHOUT_GROUPS = "(" + SQL_IDENTIFIER_PART + "(?:\\." + SQL_IDENTIFIER_PART + "){0,3}?)"; private final static String SQL_IDENTIFIER_WITH_GROUPS = "(" + SQL_IDENTIFIER_PART + ")" + "(?:\\." + "(" + SQL_IDENTIFIER_PART + "))?"; // This is used in three part name matching. static String getSQLIdentifierWithGroups() { return SQL_IDENTIFIER_WITH_GROUPS; } /* * JDBC call syntax regex From the JDBC spec: {call procedure_name} {call procedure_name(?, ?, ...)} {? = call * procedure_name[(?, ?, ...)]} allowing for arbitrary amounts of whitespace in the obvious places. */ private final static Pattern JDBC_CALL_SYNTAX = Pattern .compile("(?s)\\s*?\\{\\s*?(\\?\\s*?=)?\\s*?[cC][aA][lL][lL]\\s+?" + SQL_IDENTIFIER_WITHOUT_GROUPS + "(?:\\s*?\\((.*)\\))?\\s*\\}.*+"); /* * T-SQL EXECUTE syntax regex EXEC | EXECUTE [@return_result =] procedure_name [parameters] allowing for arbitrary * amounts of whitespace in the obvious places. */ private final static Pattern SQL_EXEC_SYNTAX = Pattern.compile("\\s*?[eE][xX][eE][cC](?:[uU][tT][eE])??\\s+?(" + SQL_IDENTIFIER_WITHOUT_GROUPS + "\\s*?=\\s+?)??" + SQL_IDENTIFIER_WITHOUT_GROUPS + "(?:$|(?:\\s+?.*+))"); /* * JDBC limit escape syntax From the JDBC spec: {LIMIT [OFFSET ]} The driver currently does not * support the OFFSET part. It will throw an exception if used. */ enum State { START, END, SUBQUERY, // also handles anything inside any (), e.g. scalar functions SELECT, OPENQUERY, OPENROWSET, LIMIT, OFFSET, QUOTE, PROCESS } // This pattern matches the LIMIT syntax with an OFFSET clause. The driver does not support OFFSET expression in the // LIMIT clause. // It will throw an exception if OFFSET is present in the LIMIT escape syntax. private final static Pattern LIMIT_SYNTAX_WITH_OFFSET = Pattern .compile("\\{\\s*[lL][iI][mM][iI][tT]\\s+(.*)\\s+[oO][fF][fF][sS][eE][tT]\\s+(.*)\\}"); // This pattern is used to determine if the query has LIMIT escape syntax. If so, then the query is further // processed to translate the syntax. private final static Pattern LIMIT_SYNTAX_GENERIC = Pattern .compile("\\{\\s*[lL][iI][mM][iI][tT]\\s+(.*)(\\s+[oO][fF][fF][sS][eE][tT](.*)\\}|\\s*\\})"); private final static Pattern SELECT_PATTERN = Pattern.compile("([sS][eE][lL][eE][cC][tT])\\s+"); // OPENQUERY ( linked_server ,'query' ) private final static Pattern OPEN_QUERY_PATTERN = Pattern .compile("[oO][pP][eE][nN][qQ][uU][eE][rR][yY]\\s*\\(.*,\\s*'(.*)'\\s*\\)"); /* * OPENROWSET ( 'provider_name', { 'datasource' ; 'user_id' ; 'password' | 'provider_string' }, { [ catalog. ] [ * schema. ] object | 'query' } ) */ private final static Pattern OPEN_ROWSET_PATTERN = Pattern .compile("[oO][pP][eE][nN][rR][oO][wW][sS][eE][tT]\\s*\\(.*,.*,\\s*'(.*)'\\s*\\)"); /* * {limit 30} {limit ?} {limit (?)} */ private final static Pattern LIMIT_ONLY_PATTERN = Pattern .compile("\\{\\s*[lL][iI][mM][iI][tT]\\s+(((\\(|\\s)*)(\\d*|\\?)((\\)|\\s)*))\\s*\\}"); /** * Translates the LIMIT escape syntax, {LIMIT [OFFSET ]} SQL Server does not support LIMIT syntax, the * LIMIT escape syntax is thus translated to use "TOP" syntax The OFFSET clause is not supported, and will throw an * exception if used. * * @param sql * the SQL query * * @param indx * Position in the query from where to start translation * * @param endChar * The character that marks the end of translation * * @throws SQLServerException * * @return the number of characters that have been translated * */ int translateLimit(StringBuffer sql, int indx, char endChar) throws SQLServerException { Matcher selectMatcher = SELECT_PATTERN.matcher(sql); Matcher openQueryMatcher = OPEN_QUERY_PATTERN.matcher(sql); Matcher openRowsetMatcher = OPEN_ROWSET_PATTERN.matcher(sql); Matcher limitMatcher = LIMIT_ONLY_PATTERN.matcher(sql); Matcher offsetMatcher = LIMIT_SYNTAX_WITH_OFFSET.matcher(sql); int startIndx = indx; Stack topPosition = new Stack<>(); State nextState = State.START; while (indx < sql.length()) { char ch = sql.charAt(indx); switch (nextState) { case START: nextState = State.PROCESS; break; case PROCESS: // The search for endChar should come before the search for quote (') as openquery has quote(') as // the endChar if (endChar == ch) { nextState = State.END; } else if ('\'' == ch) { nextState = State.QUOTE; } else if ('(' == ch) { nextState = State.SUBQUERY; } else if (limitMatcher.find(indx) && indx == limitMatcher.start()) { nextState = State.LIMIT; } else if (offsetMatcher.find(indx) && indx == offsetMatcher.start()) { nextState = State.OFFSET; } else if (openQueryMatcher.find(indx) && indx == openQueryMatcher.start()) { nextState = State.OPENQUERY; } else if (openRowsetMatcher.find(indx) && indx == openRowsetMatcher.start()) { nextState = State.OPENROWSET; } else if (selectMatcher.find(indx) && indx == selectMatcher.start()) { nextState = State.SELECT; } else indx++; break; case OFFSET: // throw exception as OFFSET is not supported // SQLState is null as this error is generated in the driver throw new SQLServerException(SQLServerException.getErrString("R_limitOffsetNotSupported"), null, 0, // Use // 0 // instead // of // DriverError.NOT_SET // to // use // the // correct // constructor null); case LIMIT: // Check if the number of opening/closing parentheses surrounding the digits or "?" in LIMIT match // Count the number of opening parentheses. int openingParentheses = 0, closingParentheses = 0; int pos = -1; String openingStr = limitMatcher.group(2); String closingStr = limitMatcher.group(5); while (-1 != (pos = openingStr.indexOf('(', pos + 1))) { openingParentheses++; } pos = -1; while (-1 != (pos = closingStr.indexOf(')', pos + 1))) { closingParentheses++; } if (openingParentheses != closingParentheses) { // SQLState is null as this error is generated in the driver throw new SQLServerException(SQLServerException.getErrString("R_limitEscapeSyntaxError"), null, 0, // Use 0 instead of DriverError.NOT_SET to use the correct constructor null); } /* * 'topPosition' is a stack that keeps track of the positions where the next "TOP" should be * inserted. The SELECT expressions are matched with the closest LIMIT expressions unless in a * subquery with explicit parentheses, that's why it needs to be a stack. To translate, we add the * clause after SELECT and delete the clause {LIMIT rows}. */ if (!topPosition.empty()) { Integer top = topPosition.pop(); String rows = limitMatcher.group(1); // Delete the LIMIT clause. sql.delete(limitMatcher.start() - 1, limitMatcher.end()); // Add the TOP clause. if ('?' == rows.charAt(0)) { // For parameterized queries the '?' needs to wrapped in parentheses. sql.insert(top, " TOP (" + rows + ")"); // add the letters/spaces inserted with TOP, add the digits from LIMIT, subtract one because // the // current letter at the index is deleted. indx += 7 + rows.length() - 1; } else { sql.insert(top, " TOP " + rows); indx += 5 + rows.length() - 1; } } else { // Could not match LIMIT with a SELECT, should never occur. // But if it does, just ignore // Matcher.end() returns offset after the last character of matched string indx = limitMatcher.end() - 1; } nextState = State.PROCESS; break; case SELECT: indx = selectMatcher.end(1); topPosition.push(indx); nextState = State.PROCESS; break; case QUOTE: // Consume the current character indx++; if (sql.length() > indx && '\'' == sql.charAt(indx)) { // Consume the quote. // If this is part of an escaped quote, stay in QUOTE state, else go to PROCESS // To escape a quote SQL Server requires two quotes indx++; if (sql.length() > indx && '\'' == sql.charAt(indx)) { nextState = State.QUOTE; } else { nextState = State.PROCESS; } } else { nextState = State.QUOTE; } break; case SUBQUERY: // Consume the opening bracket. indx++; // Consume the subquery. indx += translateLimit(sql, indx, ')'); nextState = State.PROCESS; break; case OPENQUERY: // skip the characters until query start. indx = openQueryMatcher.start(1); indx += translateLimit(sql, indx, '\''); nextState = State.PROCESS; break; case OPENROWSET: // skip the characters until query start. indx = openRowsetMatcher.start(1); indx += translateLimit(sql, indx, '\''); nextState = State.PROCESS; break; case END: // Consume the endChar character found indx++; return indx - startIndx; default: // This should never occur. // throw break; } } // end of while return indx - startIndx; } String translate(String sql) throws SQLServerException { Matcher matcher; matcher = JDBC_CALL_SYNTAX.matcher(sql); if (matcher.matches()) { // Figure out the procedure name and whether there is a return value and then // rewrite the JDBC call syntax as T-SQL EXEC syntax. hasReturnValueSyntax = (null != matcher.group(1)); procedureName = matcher.group(2); String args = matcher.group(3); sql = "EXEC " + (hasReturnValueSyntax ? "? = " : "") + procedureName + ((null != args) ? (" " + args) : ""); } else { matcher = SQL_EXEC_SYNTAX.matcher(sql); if (matcher.matches()) { // Figure out the procedure name and whether there is a return value, // but do not rewrite the statement as it is already in T-SQL EXEC syntax. hasReturnValueSyntax = (null != matcher.group(1)); procedureName = matcher.group(3); } } // Search for LIMIT escape syntax. Do further processing if present. matcher = LIMIT_SYNTAX_GENERIC.matcher(sql); if (matcher.find()) { StringBuffer sqlbuf = new StringBuffer(sql); translateLimit(sqlbuf, 0, '\0'); return sqlbuf.toString(); } // 'sql' is modified if CALL or LIMIT escape sequence is present, Otherwise pass it straight through. return sql; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy