Skip to content

Commit 0f7a423

Browse files
committed
sp_prepare support, connection string prop for prepare method
1 parent 5f51463 commit 0f7a423

File tree

13 files changed

+310
-16
lines changed

13 files changed

+310
-16
lines changed

src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerConnection.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,25 @@ CallableStatement prepareCall(String sql, int nType, int nConcur, int nHold,
278278
*/
279279
void setEnablePrepareOnFirstPreparedStatementCall(boolean value);
280280

281+
/**
282+
* Returns the behavior for a specific connection instance. Returns one of the following:
283+
* 1. prepxec, if sp_prepexec is set (default behavior)
284+
* 2. prepare, if sp_prepare is set
285+
*
286+
* @return Returns current setting for prepareMethod connection property.
287+
*/
288+
String getPrepareMethod();
289+
290+
/**
291+
* Sets the behavior for the prepare method. Only the following string values are permitted:
292+
* 1. prepexec, for use of sp_prepexec (default behavior)
293+
* 2. prepare, for use of sp_prepare
294+
*
295+
* @param prepareMethod
296+
* Changes the setting as per description
297+
*/
298+
void setPrepareMethod(String prepareMethod);
299+
281300
/**
282301
* Returns the behavior for a specific connection instance. This setting controls how many outstanding prepared
283302
* statement discard actions (sp_unprepare) can be outstanding per connection before a call to clean-up the

src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1038,7 +1038,7 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
10381038

10391039
/**
10401040
* Returns the current flag for value sendTemporalDataTypesAsStringForBulkCopy
1041-
*
1041+
*
10421042
* @return 'sendTemporalDataTypesAsStringForBulkCopy' property value.
10431043
*/
10441044
boolean getSendTemporalDataTypesAsStringForBulkCopy();
@@ -1126,4 +1126,23 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
11261126
* @return interval in seconds
11271127
*/
11281128
int getConnectRetryInterval();
1129+
1130+
/**
1131+
* Sets the behavior for the prepare method. Only the following string values are permitted:
1132+
* 1. prepexec, for use of sp_prepexec (default behavior)
1133+
* 2. prepare, for use of sp_prepare
1134+
*
1135+
* @param prepareMethod
1136+
* Changes the setting as per description
1137+
*/
1138+
void setPrepareMethod(String prepareMethod);
1139+
1140+
/**
1141+
* Returns the value indicating the prepare method. One of the following will be returned:
1142+
* 1. prepexec, for sp_prepexec (default)
1143+
* 2. prepare, for sp_prepare
1144+
*
1145+
* @return prepare method
1146+
*/
1147+
String getPrepareMethod();
11291148
}

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
123123
/** Current limit for this particular connection. */
124124
private Boolean enablePrepareOnFirstPreparedStatementCall = null;
125125

126+
private String prepareMethod = null;
127+
126128
/** Handle the actual queue of discarded prepared statements. */
127129
private ConcurrentLinkedQueue<PreparedStatementHandle> discardedPreparedStatementHandles = new ConcurrentLinkedQueue<>();
128130

@@ -2045,6 +2047,14 @@ Connection connectInternal(Properties propsIn,
20452047
}
20462048
transparentNetworkIPResolution = isBooleanPropertyOn(sPropKey, sPropValue);
20472049

2050+
sPropKey = SQLServerDriverStringProperty.PREPARE_METHOD.toString();
2051+
sPropValue = activeConnectionProperties.getProperty(sPropKey);
2052+
if (null == sPropValue) {
2053+
sPropValue = SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue();
2054+
activeConnectionProperties.setProperty(sPropKey, sPropValue);
2055+
}
2056+
setPrepareMethod(PrepareMethod.valueOfString(sPropValue).toString());
2057+
20482058
sPropKey = SQLServerDriverBooleanProperty.ENCRYPT.toString();
20492059
sPropValue = activeConnectionProperties.getProperty(sPropKey);
20502060
if (null == sPropValue) {
@@ -7222,6 +7232,20 @@ public void setEnablePrepareOnFirstPreparedStatementCall(boolean value) {
72227232
this.enablePrepareOnFirstPreparedStatementCall = value;
72237233
}
72247234

7235+
@Override
7236+
public String getPrepareMethod() {
7237+
if (null == this.prepareMethod) {
7238+
return SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue();
7239+
}
7240+
7241+
return this.prepareMethod;
7242+
}
7243+
7244+
@Override
7245+
public void setPrepareMethod(String prepareMethod) {
7246+
this.prepareMethod = prepareMethod;
7247+
}
7248+
72257249
@Override
72267250
public int getServerPreparedStatementDiscardThreshold() {
72277251
if (0 > this.serverPreparedStatementDiscardThreshold)

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionPoolProxy.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,16 @@ public void setEnablePrepareOnFirstPreparedStatementCall(boolean value) {
550550
wrappedConnection.setEnablePrepareOnFirstPreparedStatementCall(value);
551551
}
552552

553+
@Override
554+
public String getPrepareMethod() {
555+
return wrappedConnection.getPrepareMethod();
556+
}
557+
558+
@Override
559+
public void setPrepareMethod(String prepareMethod) {
560+
wrappedConnection.setPrepareMethod(prepareMethod);
561+
}
562+
553563
@Override
554564
public int getServerPreparedStatementDiscardThreshold() {
555565
return wrappedConnection.getServerPreparedStatementDiscardThreshold();

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,17 @@ public int getConnectRetryInterval() {
11251125
SQLServerDriverIntProperty.CONNECT_RETRY_INTERVAL.getDefaultValue());
11261126
}
11271127

1128+
@Override
1129+
public void setPrepareMethod(String prepareMethod) {
1130+
setStringProperty(connectionProps, SQLServerDriverStringProperty.PREPARE_METHOD.toString(), prepareMethod);
1131+
}
1132+
1133+
@Override
1134+
public String getPrepareMethod() {
1135+
return getStringProperty(connectionProps, SQLServerDriverStringProperty.PREPARE_METHOD.toString(),
1136+
SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue());
1137+
}
1138+
11281139
/**
11291140
* Sets a property string value.
11301141
*

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.text.MessageFormat;
1212
import java.util.Enumeration;
1313
import java.util.Locale;
14+
import java.util.Map;
1415
import java.util.Properties;
1516
import java.util.concurrent.atomic.AtomicInteger;
1617
import java.util.logging.Level;
@@ -334,10 +335,47 @@ public String toString() {
334335
}
335336
}
336337

338+
enum PrepareMethod {
339+
PREPEXEC("prepexec"), //sp_prepexec, default prepare method
340+
PREPARE("prepare");
341+
342+
private final String value;
343+
344+
private static final Map<String, PrepareMethod> PREPARE_METHODS = Map.of(
345+
"prepexec", PREPEXEC,
346+
"prepare", PREPARE
347+
);
348+
349+
private PrepareMethod(String value) {
350+
this.value = value;
351+
}
352+
353+
@Override
354+
public String toString() {
355+
return value;
356+
}
357+
358+
static PrepareMethod valueOfString(String value) throws SQLServerException {
359+
assert value != null;
360+
361+
if (!isValidPrepareMethod(value)) {
362+
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidConnectionSetting"));
363+
Object[] msgArgs = {SQLServerDriverStringProperty.PREPARE_METHOD.toString(), value};
364+
throw new SQLServerException(form.format(msgArgs), null);
365+
}
366+
367+
return PREPARE_METHODS.get(value);
368+
}
369+
370+
static boolean isValidPrepareMethod(String value) {
371+
return PREPARE_METHODS.containsKey(value);
372+
}
373+
}
337374

338375
enum SQLServerDriverStringProperty {
339376
APPLICATION_INTENT("applicationIntent", ApplicationIntent.READ_WRITE.toString()),
340377
APPLICATION_NAME("applicationName", SQLServerDriver.DEFAULT_APP_NAME),
378+
PREPARE_METHOD("prepareMethod", PrepareMethod.PREPEXEC.toString()),
341379
DATABASE_NAME("databaseName", ""),
342380
FAILOVER_PARTNER("failoverPartner", ""),
343381
HOSTNAME_IN_CERTIFICATE("hostNameInCertificate", ""),
@@ -517,6 +555,9 @@ public final class SQLServerDriver implements java.sql.Driver {
517555
new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString(),
518556
Boolean.toString(SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.getDefaultValue()), false,
519557
new String[] {"true"}),
558+
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.PREPARE_METHOD.toString(),
559+
SQLServerDriverStringProperty.PREPARE_METHOD.getDefaultValue(),false,
560+
new String[] {PrepareMethod.PREPEXEC.toString(), PrepareMethod.PREPARE.toString()}),
520561
new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.ENCRYPT.toString(),
521562
Boolean.toString(SQLServerDriverBooleanProperty.ENCRYPT.getDefaultValue()), false, TRUE_FALSE),
522563
new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.FAILOVER_PARTNER.toString(),

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ public class SQLServerPreparedStatement extends SQLServerStatement implements IS
7575
/** True if this execute has been called for this statement at least once */
7676
private boolean isExecutedAtLeastOnce = false;
7777

78+
/** True if sp_prepare was called **/
79+
private boolean isSpPrepareExecuted = false;
80+
7881
/** Reference to cache item for statement handle pooling. Only used to decrement ref count on statement close. */
7982
private PreparedStatementHandle cachedPreparedStatementHandle;
8083

@@ -608,7 +611,7 @@ final void doExecutePreparedStatement(PrepStmtExecCmd command) throws SQLServerE
608611
// continue using it after we return.
609612
TDSWriter tdsWriter = command.startRequest(TDS.PKT_RPC);
610613

611-
needsPrepare = doPrepExec(tdsWriter, inOutParam, hasNewTypeDefinitions, hasExistingTypeDefinitions);
614+
needsPrepare = doPrepExec(tdsWriter, inOutParam, hasNewTypeDefinitions, hasExistingTypeDefinitions, command);
612615

613616
ensureExecuteResultsReader(command.startResponse(getIsResponseBufferingAdaptive()));
614617
startResults();
@@ -760,6 +763,31 @@ private void buildServerCursorPrepExecParams(TDSWriter tdsWriter) throws SQLServ
760763
tdsWriter.writeRPCInt(null, 0, true);
761764
}
762765

766+
private void buildPrepParams(TDSWriter tdsWriter) throws SQLServerException {
767+
if (getStatementLogger().isLoggable(java.util.logging.Level.FINE))
768+
getStatementLogger().fine(toString() + ": calling sp_prepare: PreparedHandle:"
769+
+ getPreparedStatementHandle() + ", SQL:" + preparedSQL);
770+
771+
expectPrepStmtHandle = true;
772+
executedSqlDirectly = false;
773+
expectCursorOutParams = false;
774+
outParamIndexAdjustment = 4;
775+
776+
tdsWriter.writeShort((short) 0xFFFF); // procedure name length -> use ProcIDs
777+
tdsWriter.writeShort(TDS.PROCID_SP_PREPARE);
778+
tdsWriter.writeByte((byte) 0);
779+
tdsWriter.writeByte((byte) 0);
780+
tdsWriter.sendEnclavePackage(preparedSQL, enclaveCEKs);
781+
782+
tdsWriter.writeRPCInt(null, getPreparedStatementHandle(), true);
783+
resetPrepStmtHandle(false);
784+
785+
tdsWriter.writeRPCStringUnicode((preparedTypeDefinitions.length() > 0) ? preparedTypeDefinitions : null);
786+
787+
tdsWriter.writeRPCStringUnicode(preparedSQL);
788+
tdsWriter.writeRPCInt(null, 1, false);
789+
}
790+
763791
private void buildPrepExecParams(TDSWriter tdsWriter) throws SQLServerException {
764792
if (getStatementLogger().isLoggable(java.util.logging.Level.FINE))
765793
getStatementLogger().fine(toString() + ": calling sp_prepexec: PreparedHandle:"
@@ -1050,36 +1078,62 @@ private boolean reuseCachedHandle(boolean hasNewTypeDefinitions, boolean discard
10501078
private ArrayList<byte[]> enclaveCEKs;
10511079

10521080
private boolean doPrepExec(TDSWriter tdsWriter, Parameter[] params, boolean hasNewTypeDefinitions,
1053-
boolean hasExistingTypeDefinitions) throws SQLServerException {
1081+
boolean hasExistingTypeDefinitions, TDSCommand command) throws SQLServerException {
10541082

10551083
boolean needsPrepare = (hasNewTypeDefinitions && hasExistingTypeDefinitions) || !hasPreparedStatementHandle();
1084+
boolean isPrepExecEnabled = connection.getPrepareMethod().equals(PrepareMethod.PREPEXEC.toString());
10561085

10571086
// Cursors don't use statement pooling.
10581087
if (isCursorable(executeMethod)) {
1059-
10601088
if (needsPrepare)
10611089
buildServerCursorPrepExecParams(tdsWriter);
10621090
else
10631091
buildServerCursorExecParams(tdsWriter);
10641092
} else {
10651093
// Move overhead of needing to do prepare & unprepare to only use cases that need more than one execution.
1066-
// First execution, use sp_executesql, optimizing for asumption we will not re-use statement.
1094+
// First execution, use sp_executesql, optimizing for assumption we will not re-use statement.
10671095
if (needsPrepare && !connection.getEnablePrepareOnFirstPreparedStatementCall() && !isExecutedAtLeastOnce) {
10681096
buildExecSQLParams(tdsWriter);
10691097
isExecutedAtLeastOnce = true;
1070-
}
1071-
// Second execution, use prepared statements since we seem to be re-using it.
1072-
else if (needsPrepare)
1073-
buildPrepExecParams(tdsWriter);
1074-
else
1098+
} else if (needsPrepare) { // Second execution, use prepared statements since we seem to be re-using it.
1099+
if (isPrepExecEnabled) { // If true, we're using sp_prepexec.
1100+
buildPrepExecParams(tdsWriter);
1101+
} else { // Otherwise, we're using sp_prepare instead of sp_prepexec.
1102+
isSpPrepareExecuted = true;
1103+
// If we're preparing for a statement in a batch we just need to call sp_prepare b/c in the
1104+
// "batching" code it will start another tds request to execute the statement after preparing.
1105+
if (executeMethod == EXECUTE_BATCH) {
1106+
buildPrepParams(tdsWriter);
1107+
return needsPrepare;
1108+
} else { // Otherwise, if it is not a batch query, then prepare and start new TDS request to execute the statement.
1109+
isSpPrepareExecuted = false;
1110+
doPrep(tdsWriter, command);
1111+
command.startRequest(TDS.PKT_RPC);
1112+
buildExecParams(tdsWriter);
1113+
}
1114+
}
1115+
} else {
10751116
buildExecParams(tdsWriter);
1117+
}
10761118
}
10771119

10781120
sendParamsByRPC(tdsWriter, params);
10791121

10801122
return needsPrepare;
10811123
}
10821124

1125+
/**
1126+
* Executes sp_prepare to prepare a parameterized statement and sets the prepared statement handle
1127+
*
1128+
* @param tdsWriter TDS writer to write sp_prepare params to
1129+
* @throws SQLServerException
1130+
*/
1131+
private void doPrep(TDSWriter tdsWriter, TDSCommand command) throws SQLServerException {
1132+
buildPrepParams(tdsWriter);
1133+
ensureExecuteResultsReader(command.startResponse(getIsResponseBufferingAdaptive()));
1134+
command.processResponse(resultsReader());
1135+
}
1136+
10831137
@Override
10841138
public final java.sql.ResultSetMetaData getMetaData() throws SQLServerException, SQLTimeoutException {
10851139
loggerExternal.entering(getClassNameLogging(), "getMetaData");
@@ -2718,7 +2772,6 @@ final void processResponse(TDSReader tdsReader) throws SQLServerException {
27182772

27192773
final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) throws SQLServerException {
27202774
executeMethod = EXECUTE_BATCH;
2721-
27222775
batchCommand.batchException = null;
27232776
final int numBatches = batchParamValues.size();
27242777
batchCommand.updateCounts = new long[numBatches];
@@ -2826,7 +2879,7 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th
28262879
// the size of a batch's string parameter values changes such
28272880
// that repreparation is necessary.
28282881
++numBatchesPrepared;
2829-
needsPrepare = doPrepExec(tdsWriter, batchParam, hasNewTypeDefinitions, hasExistingTypeDefinitions);
2882+
needsPrepare = doPrepExec(tdsWriter, batchParam, hasNewTypeDefinitions, hasExistingTypeDefinitions, batchCommand);
28302883
if (needsPrepare || numBatchesPrepared == numBatches) {
28312884
ensureExecuteResultsReader(batchCommand.startResponse(getIsResponseBufferingAdaptive()));
28322885

@@ -2845,6 +2898,18 @@ final void doExecutePreparedStatementBatch(PrepStmtBatchExecCmd batchCommand) th
28452898
if (!getNextResult(true))
28462899
return;
28472900

2901+
if (isSpPrepareExecuted) {
2902+
isSpPrepareExecuted = false;
2903+
resetForReexecute();
2904+
tdsWriter = batchCommand.startRequest(TDS.PKT_RPC);
2905+
buildExecParams(tdsWriter);
2906+
sendParamsByRPC(tdsWriter, batchParam);
2907+
ensureExecuteResultsReader(batchCommand.startResponse(getIsResponseBufferingAdaptive()));
2908+
startResults();
2909+
if (!getNextResult(true))
2910+
return;
2911+
}
2912+
28482913
// If the result is a ResultSet (rather than an update count) then throw an
28492914
// exception for this result. The exception gets caught immediately below and
28502915
// translated into (or added to) a BatchUpdateException.

src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ protected Object[][] getContents() {
192192
{"R_failoverPartnerPropertyDescription", "The name of the failover server used in a database mirroring configuration."},
193193
{"R_packetSizePropertyDescription", "The network packet size used to communicate with SQL Server."},
194194
{"R_encryptPropertyDescription", "Determines if Secure Sockets Layer (SSL) encryption should be used between the client and the server."},
195+
{"R_prepareMethodPropertyDescription", "Determines the prepare method used in the driver."},
195196
{"R_socketFactoryClassPropertyDescription", "The class to instantiate as the SocketFactory for connections"},
196197
{"R_socketFactoryConstructorArgPropertyDescription", "The optional argument to pass to the constructor specified by socketFactoryClass"},
197198
{"R_trustServerCertificatePropertyDescription", "Determines if the driver should validate the SQL Server Secure Sockets Layer (SSL) certificate."},

src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ public void testDataSource() {
162162
ds.setEncrypt(booleanPropValue);
163163
assertEquals(booleanPropValue, ds.getEncrypt(), TestResource.getResource("R_valuesAreDifferent"));
164164

165+
ds.setPrepareMethod(stringPropValue);
166+
assertEquals(stringPropValue, ds.getPrepareMethod(), TestResource.getResource("R_valuesAreDifferent"));
167+
165168
ds.setHostNameInCertificate(stringPropValue);
166169
assertEquals(stringPropValue, ds.getHostNameInCertificate(), TestResource.getResource("R_valuesAreDifferent"));
167170

@@ -310,7 +313,7 @@ public void connectionErrorOccurred(ConnectionEvent event) {
310313
/**
311314
* Attach the Event listener and listen for connection events, fatal errors should not close the pooled connection
312315
* objects
313-
*
316+
*
314317
* @throws SQLException
315318
*/
316319
@Test
@@ -784,7 +787,7 @@ public void testGetSchema() throws SQLException {
784787

785788
/**
786789
* Test thread's interrupt status is not cleared.
787-
*
790+
*
788791
* @throws InterruptedException
789792
*/
790793
@Test

0 commit comments

Comments
 (0)